feat(settings/user/color): intro and outro animation
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import gsap from "gsap";
|
||||
|
||||
const { onRender, onClick } = useScreen();
|
||||
|
||||
const app = useAppStore();
|
||||
@@ -14,6 +16,114 @@ const CELL_SIZE = 16;
|
||||
const SPACING = 16;
|
||||
const ANIMATION_SPEED = 475;
|
||||
|
||||
const PALETTE_SLIDE_OFFSET = 96;
|
||||
const PALETTE_SLIDE_DURATION = 0.25;
|
||||
const CELL_ANIMATION_DURATION = 0.04;
|
||||
const CELL_ANIMATION_STAGGER = 0.02;
|
||||
|
||||
const SNAIL_ORDER = [0, 1, 2, 3, 7, 11, 15, 14, 13, 12, 8, 4, 5, 6, 10, 9];
|
||||
|
||||
// animation state
|
||||
const animation = reactive({
|
||||
cellVisibility: Array(16).fill(0) as number[],
|
||||
paletteOffsetY: PALETTE_SLIDE_OFFSET,
|
||||
paletteOpacity: 0,
|
||||
textOpacity: 0,
|
||||
isIntro: true,
|
||||
isOutro: false,
|
||||
});
|
||||
const isAnimating = computed(() => animation.isIntro || animation.isOutro);
|
||||
|
||||
const animateIntro = (): gsap.core.Timeline => {
|
||||
animation.isIntro = true;
|
||||
|
||||
const timeline = gsap
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
animation.isIntro = false;
|
||||
},
|
||||
})
|
||||
.to(
|
||||
animation,
|
||||
{ paletteOffsetY: 0, duration: PALETTE_SLIDE_DURATION, ease: "none" },
|
||||
0,
|
||||
)
|
||||
.to(
|
||||
animation,
|
||||
{ paletteOpacity: 1, duration: PALETTE_SLIDE_DURATION, ease: "none" },
|
||||
0,
|
||||
);
|
||||
|
||||
const colorsDuration =
|
||||
SNAIL_ORDER.length * CELL_ANIMATION_STAGGER + CELL_ANIMATION_DURATION;
|
||||
timeline.to(
|
||||
animation,
|
||||
{ textOpacity: 1, duration: colorsDuration * 0.5, ease: "none" },
|
||||
PALETTE_SLIDE_DURATION + colorsDuration * 0.5,
|
||||
);
|
||||
|
||||
for (let i = 0; i < SNAIL_ORDER.length; i++) {
|
||||
timeline.to(
|
||||
animation.cellVisibility,
|
||||
{
|
||||
[SNAIL_ORDER[i]!]: 1,
|
||||
duration: CELL_ANIMATION_DURATION,
|
||||
ease: "none",
|
||||
},
|
||||
PALETTE_SLIDE_DURATION + i * CELL_ANIMATION_STAGGER,
|
||||
);
|
||||
}
|
||||
|
||||
return timeline;
|
||||
};
|
||||
|
||||
const animateOutro = (): gsap.core.Timeline => {
|
||||
animation.isOutro = true;
|
||||
|
||||
const timeline = gsap.timeline();
|
||||
|
||||
const colorsOutroDuration =
|
||||
SNAIL_ORDER.length * CELL_ANIMATION_STAGGER + CELL_ANIMATION_DURATION;
|
||||
timeline.to(
|
||||
animation,
|
||||
{ textOpacity: 0, duration: colorsOutroDuration * 0.5, ease: "none" },
|
||||
0,
|
||||
);
|
||||
|
||||
for (let i = SNAIL_ORDER.length - 1; i >= 0; i--) {
|
||||
timeline.to(
|
||||
animation.cellVisibility,
|
||||
{
|
||||
[SNAIL_ORDER[i]!]: 0,
|
||||
duration: CELL_ANIMATION_DURATION,
|
||||
ease: "none",
|
||||
},
|
||||
(SNAIL_ORDER.length - 1 - i) * CELL_ANIMATION_STAGGER,
|
||||
);
|
||||
}
|
||||
|
||||
timeline.to(
|
||||
animation,
|
||||
{
|
||||
paletteOffsetY: PALETTE_SLIDE_OFFSET,
|
||||
duration: PALETTE_SLIDE_DURATION,
|
||||
ease: "none",
|
||||
},
|
||||
colorsOutroDuration,
|
||||
);
|
||||
timeline.to(
|
||||
animation,
|
||||
{ paletteOpacity: 0, duration: PALETTE_SLIDE_DURATION, ease: "none" },
|
||||
colorsOutroDuration,
|
||||
);
|
||||
|
||||
return timeline;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
animateIntro();
|
||||
});
|
||||
|
||||
const originalSelectedCol = app.color.col;
|
||||
const originalSelectedRow = app.color.row;
|
||||
const originalColor = app.color.hex;
|
||||
@@ -30,6 +140,8 @@ const select = (col: number, row: number) => {
|
||||
};
|
||||
|
||||
useKeyDown((key) => {
|
||||
if (isAnimating.value) return;
|
||||
|
||||
switch (key) {
|
||||
case "NDS_UP":
|
||||
if (selectedRow > 0) select(selectedCol, selectedRow - 1);
|
||||
@@ -47,6 +159,8 @@ useKeyDown((key) => {
|
||||
});
|
||||
|
||||
onClick((x, y) => {
|
||||
if (isAnimating.value) return;
|
||||
|
||||
const relativeX = x - GRID_START_X;
|
||||
const relativeY = y - GRID_START_Y;
|
||||
|
||||
@@ -65,50 +179,86 @@ onClick((x, y) => {
|
||||
});
|
||||
|
||||
onRender((ctx, deltaTime) => {
|
||||
ctx.globalAlpha = animation.paletteOpacity;
|
||||
ctx.translate(0, animation.paletteOffsetY);
|
||||
|
||||
assets.images.settings.bottomScreen.user.colorPalette.draw(ctx, 16, 32);
|
||||
|
||||
// animate
|
||||
const finalSelectorX = GRID_START_X + selectedCol * (CELL_SIZE + SPACING) - 4;
|
||||
const finalSelectorY = GRID_START_Y + selectedRow * (CELL_SIZE + SPACING) - 4;
|
||||
// colors
|
||||
for (let row = 0; row < GRID_SIZE; row++) {
|
||||
for (let col = 0; col < GRID_SIZE; col++) {
|
||||
const cellIndex = row * GRID_SIZE + col;
|
||||
const visibility = animation.cellVisibility[cellIndex]!;
|
||||
|
||||
const dx = finalSelectorX - selectorX;
|
||||
const dy = finalSelectorY - selectorY;
|
||||
if (visibility > 0) {
|
||||
const x = GRID_START_X + col * (CELL_SIZE + SPACING);
|
||||
const y = GRID_START_Y + row * (CELL_SIZE + SPACING);
|
||||
const size = Math.floor(CELL_SIZE * visibility);
|
||||
const offset = Math.floor((CELL_SIZE - size) / 2);
|
||||
|
||||
if (dx > 0) {
|
||||
selectorX += ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorX > finalSelectorX) selectorX = finalSelectorX;
|
||||
} else if (dx < 0) {
|
||||
selectorX -= ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorX < finalSelectorX) selectorX = finalSelectorX;
|
||||
} else if (dy > 0) {
|
||||
selectorY += ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorY > finalSelectorY) selectorY = finalSelectorY;
|
||||
} else if (dy < 0) {
|
||||
selectorY -= ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorY < finalSelectorY) selectorY = finalSelectorY;
|
||||
ctx.fillStyle = APP_COLORS[row]![col]!;
|
||||
ctx.fillRect(x + offset, y + offset, size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// selector
|
||||
if (!animation.isOutro) {
|
||||
const finalSelectorX =
|
||||
GRID_START_X + selectedCol * (CELL_SIZE + SPACING) - 4;
|
||||
const finalSelectorY =
|
||||
GRID_START_Y + selectedRow * (CELL_SIZE + SPACING) - 4;
|
||||
|
||||
const dx = finalSelectorX - selectorX;
|
||||
const dy = finalSelectorY - selectorY;
|
||||
|
||||
if (dx > 0) {
|
||||
selectorX += ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorX > finalSelectorX) selectorX = finalSelectorX;
|
||||
} else if (dx < 0) {
|
||||
selectorX -= ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorX < finalSelectorX) selectorX = finalSelectorX;
|
||||
} else if (dy > 0) {
|
||||
selectorY += ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorY > finalSelectorY) selectorY = finalSelectorY;
|
||||
} else if (dy < 0) {
|
||||
selectorY -= ANIMATION_SPEED * (deltaTime / 1000);
|
||||
if (selectorY < finalSelectorY) selectorY = finalSelectorY;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = APP_COLORS[selectedRow]![selectedCol]!;
|
||||
|
||||
const selectorDrawY = selectorY;
|
||||
const offsets = [0, 3, 7, 11, 15, 19, 22];
|
||||
for (const offset of offsets) {
|
||||
ctx.fillRect(selectorX + offset, selectorY + 0, 2, 1);
|
||||
ctx.fillRect(selectorX + offset, selectorY + 23, 2, 1);
|
||||
ctx.fillRect(selectorX + 0, selectorY + offset, 1, 2);
|
||||
ctx.fillRect(selectorX + 23, selectorY + offset, 1, 2);
|
||||
ctx.fillRect(selectorX + offset, selectorDrawY + 0, 2, 1);
|
||||
ctx.fillRect(selectorX + offset, selectorDrawY + 23, 2, 1);
|
||||
ctx.fillRect(selectorX + 0, selectorDrawY + offset, 1, 2);
|
||||
ctx.fillRect(selectorX + 23, selectorDrawY + offset, 1, 2);
|
||||
}
|
||||
|
||||
// preview
|
||||
ctx.fillRect(192, 96, 32, 32);
|
||||
|
||||
ctx.globalAlpha = animation.textOpacity;
|
||||
ctx.font = "7px NDS7";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.textBaseline = "top";
|
||||
fillTextHCentered(ctx, "Choose color", 177, 69, 62);
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
select(originalSelectedCol, originalSelectedRow);
|
||||
const handleActivateB = async () => {
|
||||
if (isAnimating.value) return;
|
||||
|
||||
app.setColor(originalSelectedCol, originalSelectedRow);
|
||||
await animateOutro();
|
||||
store.closeSubMenu();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
const handleActivateA = () => {
|
||||
if (isAnimating.value) return;
|
||||
|
||||
app.save();
|
||||
|
||||
if (app.color.hex !== originalColor) {
|
||||
@@ -124,7 +274,10 @@ const handleConfirm = () => {
|
||||
|
||||
confirmationModal.open({
|
||||
text: $t("settings.user.color.confirmation"),
|
||||
onClosed: () => store.closeSubMenu(),
|
||||
onClosed: async () => {
|
||||
await animateOutro();
|
||||
store.closeSubMenu();
|
||||
},
|
||||
timeout: 2000,
|
||||
});
|
||||
};
|
||||
@@ -135,7 +288,7 @@ const handleConfirm = () => {
|
||||
:y-offset="0"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
@activate-b="handleCancel"
|
||||
@activate-a="handleConfirm"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 194 B |
Reference in New Issue
Block a user