Compare commits
18 commits
57c08b5646
...
f0b1d2281e
Author | SHA1 | Date | |
---|---|---|---|
f0b1d2281e | |||
16bd966a7d | |||
bf30ca0252 | |||
26954a3bf2 | |||
20202b5cd9 | |||
b03fee8c7e | |||
1aba4f405e | |||
7688caebe1 | |||
fcad7e2bc9 | |||
d18f43d769 | |||
059644f847 | |||
83eec2db60 | |||
3f47f47ced | |||
6bc0c55000 | |||
62d9f2d24a | |||
d79b6b6c33 | |||
c8c4facb60 | |||
7ec900b6b6 |
13 changed files with 350 additions and 40 deletions
|
@ -1,4 +1,4 @@
|
||||||
class OutfitLayer extends HTMLElement {
|
class OutfitViewer extends HTMLElement {
|
||||||
#internals;
|
#internals;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -10,26 +10,98 @@ class OutfitLayer extends HTMLElement {
|
||||||
setTimeout(() => this.#connectToChildren(), 0);
|
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);
|
||||||
|
this.#setIsPlayingCookie(playPauseToggle.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#setIsPlaying(isPlaying) {
|
||||||
|
// TODO: Listen for changes to the child list, and add `playing` when new
|
||||||
|
// nodes arrive, if playing.
|
||||||
|
const thirtyDays = 60 * 60 * 24 * 30;
|
||||||
|
if (isPlaying) {
|
||||||
|
this.#internals.states.add("playing");
|
||||||
|
for (const layer of this.querySelectorAll("outfit-layer")) {
|
||||||
|
layer.play();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#internals.states.delete("playing");
|
||||||
|
for (const layer of this.querySelectorAll("outfit-layer")) {
|
||||||
|
layer.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setIsPlayingCookie(isPlaying) {
|
||||||
|
const thirtyDays = 60 * 60 * 24 * 30;
|
||||||
|
const value = isPlaying ? "true" : "false";
|
||||||
|
document.cookie = `DTIOutfitViewerIsPlaying=${value};max-age=${thirtyDays}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OutfitLayer extends HTMLElement {
|
||||||
|
#internals;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#internals = this.attachInternals();
|
||||||
|
|
||||||
|
// An <outfit-layer> starts in the loading state, and then might very
|
||||||
|
// quickly decide it's not after `#connectToChildren`. This is to prevent a
|
||||||
|
// flash of *non*-loading state, when a new layer loads in. (e.g. In the
|
||||||
|
// time between our parent <turbo-frame> loading, which shows the loading
|
||||||
|
// spinner; and us being marked `:state(loading)`, which shows the loading
|
||||||
|
// spinner; we don't want the loading spinner to do its usual *immediate*
|
||||||
|
// total fade-out; then have to fade back in again, on the usual delay.)
|
||||||
|
this.#setStatus("loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
setTimeout(() => this.#connectToChildren(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
window.removeEventListener("message", this.#onMessage);
|
window.removeEventListener("message", this.#onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
this.#sendMessageToIframe({ type: "play" });
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.#sendMessageToIframe({ type: "pause" });
|
||||||
|
}
|
||||||
|
|
||||||
#connectToChildren() {
|
#connectToChildren() {
|
||||||
const image = this.querySelector("img");
|
const image = this.querySelector("img");
|
||||||
const iframe = this.querySelector("iframe");
|
const iframe = this.querySelector("iframe");
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
|
// Initialize status based on the image's current `complete` attribute,
|
||||||
|
// then wait for load/error events to update it further if needed.
|
||||||
|
this.#setStatus(image.complete ? "loaded" : "loading");
|
||||||
image.addEventListener("load", () => this.#setStatus("loaded"));
|
image.addEventListener("load", () => this.#setStatus("loaded"));
|
||||||
image.addEventListener("error", () => this.#setStatus("error"));
|
image.addEventListener("error", () => this.#setStatus("error"));
|
||||||
this.#setStatus(image.complete ? "loaded" : "loading");
|
|
||||||
} else if (iframe) {
|
} else if (iframe) {
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
window.addEventListener("message", (m) => this.#onMessage(m));
|
|
||||||
|
// Initialize status to `loading`, and asynchronously request a status
|
||||||
|
// message from the iframe if it managed to load before this triggers
|
||||||
|
// (impressive, but I think I've seen it happen!). Then, wait for
|
||||||
|
// messages or error events from the iframe to update status further if
|
||||||
|
// needed.
|
||||||
this.#setStatus("loading");
|
this.#setStatus("loading");
|
||||||
|
this.#sendMessageToIframe({ type: "requestStatus" });
|
||||||
|
window.addEventListener("message", (m) => this.#onMessage(m));
|
||||||
|
this.iframe.addEventListener("error", () => this.#setStatus("error"));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(`<outfit-layer> must contain an <img> or <iframe> tag`);
|
||||||
`<outfit-layer> must contain an <img> or <iframe> tag`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,11 +110,17 @@ class OutfitLayer extends HTMLElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (data.type === "status") {
|
||||||
data.type === "status" &&
|
if (data.status === "loaded") {
|
||||||
["loaded", "error"].includes(data.status)
|
this.#setStatus("loaded");
|
||||||
) {
|
this.#setHasAnimations(data.hasAnimations);
|
||||||
this.#setStatus(data.status);
|
} 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)}`,
|
||||||
|
@ -51,11 +129,31 @@ 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendMessageToIframe(message) {
|
||||||
|
if (this.iframe?.contentWindow == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The frame is sandboxed (origin == null), so send to Any origin.
|
||||||
|
this.iframe.contentWindow.postMessage(message, "*");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,44 @@ 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendStatus() {
|
||||||
|
if (loadingStatus === "loading") {
|
||||||
|
sendMessage({ type: "status", status: "loading" });
|
||||||
|
} else if (loadingStatus === "loaded") {
|
||||||
|
sendMessage({
|
||||||
|
type: "status",
|
||||||
|
status: "loaded",
|
||||||
|
hasAnimations: hasAnimations(movieClip),
|
||||||
|
});
|
||||||
|
} else if (loadingStatus === "error") {
|
||||||
|
sendMessage({ type: "status", status: "error" });
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`unexpected loadingStatus ${JSON.stringify(loadingStatus)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(message) {
|
||||||
|
parent.postMessage(message, document.location.origin);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
updateCanvasDimensions();
|
updateCanvasDimensions();
|
||||||
|
|
||||||
|
@ -296,6 +332,8 @@ window.addEventListener("message", ({ data }) => {
|
||||||
play();
|
play();
|
||||||
} else if (data.type === "pause") {
|
} else if (data.type === "pause") {
|
||||||
pause();
|
pause();
|
||||||
|
} else if (data.type === "requestStatus") {
|
||||||
|
sendStatus();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unexpected message: ${JSON.stringify(data)}`);
|
throw new Error(`unexpected message: ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
|
@ -303,19 +341,13 @@ window.addEventListener("message", ({ data }) => {
|
||||||
|
|
||||||
startMovie()
|
startMovie()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
parent.postMessage(
|
sendStatus();
|
||||||
{ type: "status", status: "loaded" },
|
|
||||||
document.location.origin,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(logPrefix, error);
|
console.error(logPrefix, error);
|
||||||
|
|
||||||
loadingStatus = "error";
|
loadingStatus = "error";
|
||||||
parent.postMessage(
|
sendStatus();
|
||||||
{ type: "status", status: "error" },
|
|
||||||
document.location.origin,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If loading the movie fails, show the fallback image instead, by moving
|
// If loading the movie fails, show the fallback image instead, by moving
|
||||||
// it out of the canvas content and into the body.
|
// it out of the canvas content and into the body.
|
||||||
|
|
64
app/assets/stylesheets/application/hanger-spinner.css
Normal file
64
app/assets/stylesheets/application/hanger-spinner.css
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
.hanger-spinner {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation: 1.2s infinite hanger-spinner-swing;
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
animation: 1.6s infinite hanger-spinner-fade-pulse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Adapted from animate.css "swing". We spend 75% of the time swinging,
|
||||||
|
then 25% of the time pausing before the next loop.
|
||||||
|
|
||||||
|
We use this animation for folks who are okay with dizzy-ish motion.
|
||||||
|
For reduced motion, we use a pulse-fade instead.
|
||||||
|
*/
|
||||||
|
@keyframes hanger-spinner-swing {
|
||||||
|
15% {
|
||||||
|
transform: rotate3d(0, 0, 1, 15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: rotate3d(0, 0, 1, -10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
45% {
|
||||||
|
transform: rotate3d(0, 0, 1, 5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate3d(0, 0, 1, -5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: rotate3d(0, 0, 1, 0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate3d(0, 0, 1, 0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
A homebrew fade-pulse animation. We use this for folks who don't
|
||||||
|
like motion. It's an important accessibility thing!
|
||||||
|
*/
|
||||||
|
@keyframes hanger-spinner-fade-pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,31 +38,118 @@ body.items-show
|
||||||
height: 16px
|
height: 16px
|
||||||
width: 16px
|
width: 16px
|
||||||
|
|
||||||
.outfit-viewer
|
outfit-viewer
|
||||||
position: relative
|
|
||||||
display: block
|
display: block
|
||||||
|
position: relative
|
||||||
width: 300px
|
width: 300px
|
||||||
height: 300px
|
height: 300px
|
||||||
border: 1px solid $module-border-color
|
border: 1px solid $module-border-color
|
||||||
border-radius: 1em
|
border-radius: 1em
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
||||||
margin: 0 auto .75em
|
margin: 0 auto .75em
|
||||||
|
|
||||||
|
// There's no useful text in here, but double-clicking the play/pause
|
||||||
|
// button can cause a weird selection state. Disable text selection.
|
||||||
|
user-select: none
|
||||||
|
-webkit-user-select: none
|
||||||
|
|
||||||
outfit-layer
|
outfit-layer
|
||||||
display: block
|
display: block
|
||||||
position: absolute
|
position: absolute
|
||||||
inset: 0
|
inset: 0
|
||||||
|
|
||||||
|
// We disable pointer-events most importantly for the iframes, which
|
||||||
|
// will ignore our `cursor: wait` and show a plain cursor for the
|
||||||
|
// inside of its own document. But also, the context menus for these
|
||||||
|
// elements are kinda actively misleading, too!
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
img, iframe
|
img, iframe
|
||||||
width: 100%
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
&:has(outfit-layer:state(loading))
|
.loading-indicator
|
||||||
background: gray
|
position: absolute
|
||||||
|
z-index: 1000
|
||||||
|
bottom: 0px
|
||||||
|
right: 4px
|
||||||
|
padding: 8px
|
||||||
|
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
|
||||||
|
|
||||||
&:has(outfit-layer:state(error))
|
opacity: 0
|
||||||
border-color: $error-border-color
|
transition: opacity .5s
|
||||||
|
|
||||||
|
.play-pause-button
|
||||||
|
position: absolute
|
||||||
|
z-index: 1001
|
||||||
|
left: 8px
|
||||||
|
bottom: 8px
|
||||||
|
display: none
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
color: white
|
||||||
|
background: rgba(0, 0, 0, 0.64)
|
||||||
|
width: 2.5em
|
||||||
|
height: 2.5em
|
||||||
|
border-radius: 100%
|
||||||
|
border: 2px solid transparent
|
||||||
|
transition: all .25s
|
||||||
|
|
||||||
|
.playing-label, .paused-label
|
||||||
|
display: none
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
|
||||||
|
.play-pause-toggle
|
||||||
|
// Visually hidden
|
||||||
|
clip: rect(0 0 0 0)
|
||||||
|
clip-path: inset(50%)
|
||||||
|
height: 1px
|
||||||
|
overflow: hidden
|
||||||
|
position: absolute
|
||||||
|
white-space: nowrap
|
||||||
|
width: 1px
|
||||||
|
|
||||||
|
&:checked ~ .playing-label
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&:not(:checked) ~ .paused-label
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&:hover, &:has(.play-pause-toggle:focus)
|
||||||
|
border: 2px solid $module-border-color
|
||||||
|
background: $module-bg-color
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
|
&:has(.play-pause-toggle:active)
|
||||||
|
transform: translateY(2px)
|
||||||
|
|
||||||
|
&:has(outfit-layer:state(has-animations))
|
||||||
|
.play-pause-button
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
.error-indicator
|
||||||
|
font-size: 85%
|
||||||
|
color: $error-color
|
||||||
|
margin-top: .25em
|
||||||
|
margin-bottom: .5em
|
||||||
|
display: none
|
||||||
|
|
||||||
|
// When loading, fade in the loading spinner after a brief delay. (We only
|
||||||
|
// 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))
|
||||||
|
cursor: wait
|
||||||
|
.loading-indicator
|
||||||
|
opacity: 1
|
||||||
|
transition-delay: 2s
|
||||||
|
|
||||||
|
#item-preview:has(outfit-layer:state(error))
|
||||||
|
outfit-viewer
|
||||||
|
border: 2px solid red
|
||||||
|
.error-indicator
|
||||||
|
display: block
|
||||||
|
|
||||||
.species-color-picker
|
.species-color-picker
|
||||||
.error-icon
|
.error-icon
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#asset-canvas,
|
#asset-canvas,
|
||||||
|
#asset-image,
|
||||||
#fallback {
|
#fallback {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -15,7 +15,7 @@ class SwfAssetsController < ApplicationController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.script_src_elem -> {
|
policy.script_src -> {
|
||||||
src_list(
|
src_list(
|
||||||
helpers.javascript_url("lib/easeljs.min"),
|
helpers.javascript_url("lib/easeljs.min"),
|
||||||
helpers.javascript_url("lib/tweenjs.min"),
|
helpers.javascript_url("lib/tweenjs.min"),
|
||||||
|
@ -24,7 +24,7 @@ class SwfAssetsController < ApplicationController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.style_src_elem -> {
|
policy.style_src -> {
|
||||||
src_list(
|
src_list(
|
||||||
helpers.stylesheet_url("swf_assets/show"),
|
helpers.stylesheet_url("swf_assets/show"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -224,6 +224,10 @@ module ItemsHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def outfit_viewer_is_playing
|
||||||
|
cookies["DTIOutfitViewerIsPlaying"] == "true"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_on_pet_types(species, special_color=nil, &block)
|
def build_on_pet_types(species, special_color=nil, &block)
|
||||||
|
|
6
app/views/application/_hanger_spinner.html
Normal file
6
app/views/application/_hanger_spinner.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg class="hanger-spinner" viewBox="0 0 473 473">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M451.426,315.003c-0.517-0.344-1.855-0.641-2.41-0.889l-201.09-88.884v-28.879c38.25-4.6,57.136-29.835,57.136-62.28c0-35.926-25.283-63.026-59.345-63.026c-35.763,0-65.771,29.481-65.771,64.384c0,6.005,4.973,10.882,10.978,10.882c1.788,0,3.452-0.535,4.934-1.291c3.519-1.808,6.024-5.365,6.024-9.591c0-22.702,20.674-42.62,44.217-42.62c22.003,0,37.982,17.356,37.982,41.262c0,23.523-19.011,41.262-44.925,41.262c-6.005,0-10.356,4.877-10.356,10.882v21.267v21.353c0,0.21-0.421,0.383-0.401,0.593L35.61,320.55C7.181,330.792-2.554,354.095,0.554,371.881c3.194,18.293,18.704,30.074,38.795,30.074H422.26c23.782,0,42.438-12.307,48.683-32.942C477.11,348.683,469.078,326.766,451.426,315.003z M450.115,364.031c-3.452,11.427-13.607,18.8-27.846,18.8H39.349c-9.725,0-16.104-5.394-17.5-13.368c-1.587-9.104,4.265-22.032,21.831-28.42l199.531-94.583l196.844,87.65C449.303,340.717,453.434,353.072,450.115,364.031z"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 979 B |
|
@ -1,4 +1,16 @@
|
||||||
.outfit-viewer
|
%outfit-viewer
|
||||||
|
.loading-indicator= render partial: "hanger_spinner"
|
||||||
|
|
||||||
|
%label.play-pause-button{title: "Pause/play animations"}
|
||||||
|
%input.play-pause-toggle{
|
||||||
|
type: "checkbox",
|
||||||
|
checked: outfit_viewer_is_playing,
|
||||||
|
}
|
||||||
|
%svg.playing-label{viewBox: "0 0 24 24", "aria-hidden": "true", "aria-label": "Pause"}
|
||||||
|
%path{fill: "currentColor", d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z"}
|
||||||
|
%svg.paused-label{viewBox: "0 0 24 24", "aria-hidden": "true", "aria-label": "Play"}
|
||||||
|
%path{fill: "currentColor", d: "M8 5v14l11-7z"}
|
||||||
|
|
||||||
- outfit.visible_layers.each do |swf_asset|
|
- outfit.visible_layers.each do |swf_asset|
|
||||||
%outfit-layer{
|
%outfit-layer{
|
||||||
data: {
|
data: {
|
||||||
|
@ -7,6 +19,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, playing: outfit_viewer_is_playing ? true : nil)}
|
||||||
- else
|
- else
|
||||||
= image_tag swf_asset.image_url, alt: ""
|
= image_tag swf_asset.image_url, alt: ""
|
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
= turbo_frame_tag "item-preview" do
|
= turbo_frame_tag "item-preview" do
|
||||||
= render partial: "outfit_viewer", locals: {outfit: @preview_outfit}
|
= render partial: "outfit_viewer", locals: {outfit: @preview_outfit}
|
||||||
|
.error-indicator
|
||||||
|
💥 We couldn't load all of this outfit. Try again?
|
||||||
|
|
||||||
= form_for item_path(@item), method: :get, class: "species-color-picker",
|
= form_for item_path(@item), method: :get, class: "species-color-picker",
|
||||||
data: {"is-valid": @preview_error.nil?} do |f|
|
data: {"is-valid": @preview_error.nil?} do |f|
|
||||||
|
@ -39,6 +41,9 @@
|
||||||
%li= link_to(contributor.name, user_contributions_path(contributor)) + format_contribution_count(count)
|
%li= link_to(contributor.name, user_contributions_path(contributor)) + format_contribution_count(count)
|
||||||
%footer= t '.contributors.footer'
|
%footer= t '.contributors.footer'
|
||||||
|
|
||||||
|
- content_for :stylesheets do
|
||||||
|
= stylesheet_link_tag "application/hanger-spinner"
|
||||||
|
|
||||||
- content_for :javascripts do
|
- content_for :javascripts do
|
||||||
= javascript_include_tag "lib/idiomorph", async: true
|
= javascript_include_tag "lib/idiomorph", async: true
|
||||||
= javascript_include_tag "outfit-viewer", async: true
|
= javascript_include_tag "outfit-viewer", async: true
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||||
= yield :stylesheets
|
= yield :stylesheets
|
||||||
= stylesheet_link_tag "application"
|
= stylesheet_link_tag "application"
|
||||||
= render 'analytics'
|
|
||||||
= yield :meta
|
= yield :meta
|
||||||
= open_graph_tags
|
= open_graph_tags
|
||||||
= csrf_meta_tag
|
= csrf_meta_tag
|
||||||
|
@ -24,6 +23,7 @@
|
||||||
= javascript_include_tag 'application', defer: true
|
= javascript_include_tag 'application', defer: true
|
||||||
= yield :javascripts
|
= yield :javascripts
|
||||||
= yield :head
|
= yield :head
|
||||||
|
= render 'analytics'
|
||||||
%body{:class => body_class}
|
%body{:class => body_class}
|
||||||
#container
|
#container
|
||||||
= yield :before_title
|
= yield :before_title
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
Embed for Asset ##{@swf_asset.id} | #{t "app_name"}
|
Embed for Asset ##{@swf_asset.id} | #{t "app_name"}
|
||||||
%link{href: image_path("favicon.png"), rel: "icon"}
|
%link{href: image_path("favicon.png"), rel: "icon"}
|
||||||
|
|
||||||
-# NOTE: For all these assets, the Content-Security-Policy doesn't account
|
|
||||||
-# for asset debug mode, so let's just opt out of it with `debug: false`!
|
|
||||||
- if @swf_asset.canvas_movie?
|
|
||||||
-# Load the stylesheet first, because displaying things correctly is the
|
-# Load the stylesheet first, because displaying things correctly is the
|
||||||
-# actual most essential thing.
|
-# actual most essential thing.
|
||||||
= stylesheet_link_tag "swf_assets/show", debug: false
|
= stylesheet_link_tag "swf_assets/show", debug: false
|
||||||
|
|
||||||
|
-# NOTE: For all these assets, the Content-Security-Policy doesn't account
|
||||||
|
-# for asset debug mode, so let's just opt out of it with `debug: false`!
|
||||||
|
- if @swf_asset.canvas_movie?
|
||||||
-# This is optional, but preloading the sprites can help us from having
|
-# This is optional, but preloading the sprites can help us from having
|
||||||
-# to wait on all the other JS to load and set up before we start!
|
-# to wait on all the other JS to load and set up before we start!
|
||||||
- @swf_asset.canvas_movie_sprite_urls.each do |sprite_url|
|
- @swf_asset.canvas_movie_sprite_urls.each do |sprite_url|
|
||||||
|
@ -33,4 +33,4 @@
|
||||||
-# the browser won't bother to load it if it's not used.
|
-# the browser won't bother to load it if it's not used.
|
||||||
= image_tag @swf_asset.image_url, id: "fallback", alt: "", loading: "lazy"
|
= image_tag @swf_asset.image_url, id: "fallback", alt: "", loading: "lazy"
|
||||||
- else
|
- else
|
||||||
= image_tag @swf_asset.image_url, alt: ""
|
= image_tag @swf_asset.image_url, alt: "", id: "asset-image"
|
|
@ -74,6 +74,7 @@ namespace :public_data do
|
||||||
# The connection details for our database!
|
# The connection details for our database!
|
||||||
config = ApplicationRecord.connection_db_config.configuration_hash
|
config = ApplicationRecord.connection_db_config.configuration_hash
|
||||||
args << "--host=#{config[:host]}" if config[:host]
|
args << "--host=#{config[:host]}" if config[:host]
|
||||||
|
args << "--ssl=false" # SSL is the default for recent MariaDB; override!
|
||||||
args << "--user=#{config[:username]}" if config[:username]
|
args << "--user=#{config[:username]}" if config[:username]
|
||||||
args << "--password=#{config[:password]}" if config[:password]
|
args << "--password=#{config[:password]}" if config[:password]
|
||||||
args << "--database=#{config.fetch(:database)}"
|
args << "--database=#{config.fetch(:database)}"
|
||||||
|
|
Loading…
Reference in a new issue