Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add animations for animated textures inside the gallery #154

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions pages/gallery/gallery-animation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script lang="ts">
import { defineComponent } from 'vue';

interface AnimationFrame {
index: number;
time: number;
}

interface Animation {
frames?: (number | AnimationFrame)[];
frametime?: number;
interpolate?: boolean;
}

export default defineComponent({
name: 'AnimationCanvas',
props: {
src: { type: String, required: true },
mcmeta: { type: Object as () => { animation?: Animation }, default: () => ({ animation: {} }) },
isTiled: { type: Boolean, default: false },
},
data() {
return {
canvasRef: null as HTMLCanvasElement | null,
image: null as HTMLImageElement | null,
sprites: [] as AnimationFrame[],
frames: {} as Record<number, [AnimationFrame, number][]>,
currentTick: 1,
tickingRef: null as ReturnType<typeof setInterval> | null,
};
},
methods: {
loadImage() {
const img = new Image();
img.setAttribute('crossorigin', 'anonymous');
img.src = this.src;

img.onload = () => {
this.image = img;
this.tickingRef = setInterval(() => {}, 1000 / 20);
};

img.onerror = () => {
this.image = null;
if (this.tickingRef) {
clearInterval(this.tickingRef);
this.tickingRef = null;
}
};
},
calculateFrames() {
if (!this.image || !this.mcmeta) return;

const animation = this.mcmeta.animation ?? {};
const animationFrames: AnimationFrame[] = [];

if (animation.frames) {
for (const frame of animation.frames) {
if (typeof frame === 'object') {
animationFrames.push({ index: frame.index, time: Math.max(frame.time, 1) });
} else {
animationFrames.push({ index: frame, time: animation.frametime ?? 1 });
}
}
} else {
const framesCount = this.isTiled
? this.image.height / 2 / (this.image.width / 2)
: this.image.height / this.image.width;
for (let fi = 0; fi < framesCount; fi++) {
animationFrames.push({ index: fi, time: animation.frametime ?? 1 });
}
}

const framesToPlay: Record<number, [AnimationFrame, number][]> = {};
let ticks = 1;
animationFrames.forEach((frame, index) => {
for (let t = 1; t <= frame.time; t++) {
framesToPlay[ticks] = [[frame, 1]];

if (animation.interpolate) {
const nextFrame = animationFrames[index + 1] ?? animationFrames[0];
framesToPlay[ticks]?.push([nextFrame, t / nextFrame.time]);
}

ticks++;
}
});

this.sprites = animationFrames;
this.frames = framesToPlay;
},
updateCanvas() {
if (Object.keys(this.frames).length === 0) return;

setTimeout(() => {
let next = this.currentTick + 1;
if (this.frames[next] === undefined) next = 1;
this.currentTick = next;

this.updateCanvas();
}, 1000 / 20);
},
drawFrame() {
if (Object.keys(this.frames).length === 0) return;

const framesToDraw = this.frames[this.currentTick];
const canvas = this.$refs.canvasRef as HTMLCanvasElement;
const context = canvas?.getContext('2d');

if (!canvas || !context || !this.image || !framesToDraw) return;

canvas.style.width = '100%';
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetWidth;

const padding = this.isTiled ? this.image.width / 4 : 0;
const width = this.isTiled ? this.image.width / 2 : this.image.width;

context.clearRect(0, 0, width, width);
context.globalAlpha = 1;
context.imageSmoothingEnabled = false;

for (const frame of framesToDraw) {
const [data, alpha] = frame;
context.globalAlpha = alpha;

context.drawImage(
this.image,
padding,
padding + (width * data.index) * (this.isTiled ? 2 : 1),
width,
width,
0,
0,
canvas.width,
canvas.width
);
}
},
},
watch: {
src: 'loadImage',
mcmeta: 'calculateFrames',
image: 'calculateFrames',
currentTick: 'drawFrame',
frames: 'updateCanvas',
},
mounted() {
this.loadImage();
},
});
</script>

<template>
<canvas ref="canvasRef"></canvas>
</template>
59 changes: 52 additions & 7 deletions pages/gallery/gallery-image.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@
:style="exists ? {} : { background: 'rgba(0,0,0,0.3)' }"
>
<!-- send click events back to caller -->
<gallery-animation
v-if="exists && hasAnimation"
class="gallery-texture-image"
:src="imageURL"
:mcmeta="animation"
:isTiled="imageURL.includes('_flow')"
@click="$emit('click')"
/>
<img
v-if="exists"
class="gallery-texture-image"
:src="imageURL"
style="aspect-ratio: 1"
ref="imageRef"
:style="{
aspectRatio: 1,
opacity: hasAnimation ? 0 : 1 // allow the texture to be copied even if animation is present
}"
@error="textureNotFound"
@click="$emit('click')"
:src="imageURL"
lazy-src="https://database.faithfulpack.net/images/bot/loading.gif"
/>
<div v-else class="not-done">
Expand All @@ -23,10 +35,18 @@
</div>
</template>

<script>
<script lang="ts">
/* global settings */
import axios from "axios";

import GalleryAnimation from "./gallery-animation.vue";

// separate component to track state more easily
export default {
name: "gallery-image",
name: "gallery-image",
components: {
GalleryAnimation,
},
props: {
src: {
type: String,
Expand All @@ -43,15 +63,18 @@ export default {
},
// saves a request on every gallery image to provide it once
ignoreList: {
type: Array,
type: Array as () => string[],
required: false,
default: () => [],
},
},
data() {
return {
exists: true,
imageURL: "",
imageURL: this.src,
imageRef: null as HTMLImageElement | null,
hasAnimation: false,
animation: {},
};
},
methods: {
Expand All @@ -62,9 +85,31 @@ export default {
// if not ignored, texture hasn't been made
else this.exists = false;
},
async fetchAnimation() {
try {
const res = await axios.get(`${this.imageURL}.mcmeta`);

this.hasAnimation = true;
this.animation = res.data;
} catch {
this.hasAnimation = false;
}
},
},
created() {
this.imageURL = this.src;
const image = new Image() as HTMLImageElement;
image.src = this.src;

image.onload = () => {
// avoid (almost all) unnecessary requests
// and make sure the image is square
if (image.height % image.width === 0 && image.height !== image.width) {
this.fetchAnimation();
}
};
image.onerror = () => {
this.textureNotFound();
};
},
};
</script>