Skip to content

Commit

Permalink
Add animations in gallery (proof of concept need optimisations)
Browse files Browse the repository at this point in the history
  • Loading branch information
Juknum committed Jan 14, 2025
1 parent ffc6f96 commit f76bf3d
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 6 deletions.
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>
53 changes: 47 additions & 6 deletions pages/gallery/gallery-image.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@
>
<!-- send click events back to caller -->
<img
v-if="exists"
v-if="exists && !hasAnimation"
class="gallery-texture-image"
:src="imageURL"
ref="imageRef"
style="aspect-ratio: 1"
@error="textureNotFound"
@click="$emit('click')"
:src="imageURL"
lazy-src="https://database.faithfulpack.net/images/bot/loading.gif"
/>
<gallery-animation
v-else-if="exists && hasAnimation"
class="gallery-texture-image"
:src="imageURL"
:mcmeta="animation"
:isTiled="imageURL.includes('_flow')"
@click="$emit('click')"
/>
<div v-else class="not-done">
<span style="height: 100%" />
<!-- no idea why this div is needed but it is -->
Expand All @@ -23,10 +32,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 @@ -51,7 +68,10 @@ export default {
data() {
return {
exists: true,
imageURL: "",
imageURL: this.src,
imageRef: null as HTMLImageElement | null,
hasAnimation: false,
animation: {},
};
},
methods: {
Expand All @@ -62,9 +82,30 @@ export default {
// if not ignored, texture hasn't been made
else this.exists = false;
},
async fetchAnimation() {
try {
const res = await axios.get(`${this.src}.mcmeta`);
this.hasAnimation = true;
this.animation = res.data;
} catch {
this.hasAnimation = false;
}
},
},
created() {
this.imageURL = this.src;
const image = this.$refs.imageRef ?? 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>

0 comments on commit f76bf3d

Please sign in to comment.