feat(settings/user/color): intro and outro animation
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import gsap from "gsap";
|
||||||
|
|
||||||
const { onRender, onClick } = useScreen();
|
const { onRender, onClick } = useScreen();
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
@@ -14,6 +16,114 @@ const CELL_SIZE = 16;
|
|||||||
const SPACING = 16;
|
const SPACING = 16;
|
||||||
const ANIMATION_SPEED = 475;
|
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 originalSelectedCol = app.color.col;
|
||||||
const originalSelectedRow = app.color.row;
|
const originalSelectedRow = app.color.row;
|
||||||
const originalColor = app.color.hex;
|
const originalColor = app.color.hex;
|
||||||
@@ -30,6 +140,8 @@ const select = (col: number, row: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useKeyDown((key) => {
|
useKeyDown((key) => {
|
||||||
|
if (isAnimating.value) return;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "NDS_UP":
|
case "NDS_UP":
|
||||||
if (selectedRow > 0) select(selectedCol, selectedRow - 1);
|
if (selectedRow > 0) select(selectedCol, selectedRow - 1);
|
||||||
@@ -47,6 +159,8 @@ useKeyDown((key) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onClick((x, y) => {
|
onClick((x, y) => {
|
||||||
|
if (isAnimating.value) return;
|
||||||
|
|
||||||
const relativeX = x - GRID_START_X;
|
const relativeX = x - GRID_START_X;
|
||||||
const relativeY = y - GRID_START_Y;
|
const relativeY = y - GRID_START_Y;
|
||||||
|
|
||||||
@@ -65,11 +179,35 @@ onClick((x, y) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onRender((ctx, deltaTime) => {
|
onRender((ctx, deltaTime) => {
|
||||||
|
ctx.globalAlpha = animation.paletteOpacity;
|
||||||
|
ctx.translate(0, animation.paletteOffsetY);
|
||||||
|
|
||||||
assets.images.settings.bottomScreen.user.colorPalette.draw(ctx, 16, 32);
|
assets.images.settings.bottomScreen.user.colorPalette.draw(ctx, 16, 32);
|
||||||
|
|
||||||
// animate
|
// colors
|
||||||
const finalSelectorX = GRID_START_X + selectedCol * (CELL_SIZE + SPACING) - 4;
|
for (let row = 0; row < GRID_SIZE; row++) {
|
||||||
const finalSelectorY = GRID_START_Y + selectedRow * (CELL_SIZE + SPACING) - 4;
|
for (let col = 0; col < GRID_SIZE; col++) {
|
||||||
|
const cellIndex = row * GRID_SIZE + col;
|
||||||
|
const visibility = animation.cellVisibility[cellIndex]!;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 dx = finalSelectorX - selectorX;
|
||||||
const dy = finalSelectorY - selectorY;
|
const dy = finalSelectorY - selectorY;
|
||||||
@@ -87,28 +225,40 @@ onRender((ctx, deltaTime) => {
|
|||||||
selectorY -= ANIMATION_SPEED * (deltaTime / 1000);
|
selectorY -= ANIMATION_SPEED * (deltaTime / 1000);
|
||||||
if (selectorY < finalSelectorY) selectorY = finalSelectorY;
|
if (selectorY < finalSelectorY) selectorY = finalSelectorY;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// selector
|
|
||||||
ctx.fillStyle = APP_COLORS[selectedRow]![selectedCol]!;
|
ctx.fillStyle = APP_COLORS[selectedRow]![selectedCol]!;
|
||||||
|
|
||||||
|
const selectorDrawY = selectorY;
|
||||||
const offsets = [0, 3, 7, 11, 15, 19, 22];
|
const offsets = [0, 3, 7, 11, 15, 19, 22];
|
||||||
for (const offset of offsets) {
|
for (const offset of offsets) {
|
||||||
ctx.fillRect(selectorX + offset, selectorY + 0, 2, 1);
|
ctx.fillRect(selectorX + offset, selectorDrawY + 0, 2, 1);
|
||||||
ctx.fillRect(selectorX + offset, selectorY + 23, 2, 1);
|
ctx.fillRect(selectorX + offset, selectorDrawY + 23, 2, 1);
|
||||||
ctx.fillRect(selectorX + 0, selectorY + offset, 1, 2);
|
ctx.fillRect(selectorX + 0, selectorDrawY + offset, 1, 2);
|
||||||
ctx.fillRect(selectorX + 23, selectorY + offset, 1, 2);
|
ctx.fillRect(selectorX + 23, selectorDrawY + offset, 1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// preview
|
// preview
|
||||||
ctx.fillRect(192, 96, 32, 32);
|
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 = () => {
|
const handleActivateB = async () => {
|
||||||
select(originalSelectedCol, originalSelectedRow);
|
if (isAnimating.value) return;
|
||||||
|
|
||||||
|
app.setColor(originalSelectedCol, originalSelectedRow);
|
||||||
|
await animateOutro();
|
||||||
store.closeSubMenu();
|
store.closeSubMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleActivateA = () => {
|
||||||
|
if (isAnimating.value) return;
|
||||||
|
|
||||||
app.save();
|
app.save();
|
||||||
|
|
||||||
if (app.color.hex !== originalColor) {
|
if (app.color.hex !== originalColor) {
|
||||||
@@ -124,7 +274,10 @@ const handleConfirm = () => {
|
|||||||
|
|
||||||
confirmationModal.open({
|
confirmationModal.open({
|
||||||
text: $t("settings.user.color.confirmation"),
|
text: $t("settings.user.color.confirmation"),
|
||||||
onClosed: () => store.closeSubMenu(),
|
onClosed: async () => {
|
||||||
|
await animateOutro();
|
||||||
|
store.closeSubMenu();
|
||||||
|
},
|
||||||
timeout: 2000,
|
timeout: 2000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -135,7 +288,7 @@ const handleConfirm = () => {
|
|||||||
:y-offset="0"
|
:y-offset="0"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="$t('common.cancel')"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="$t('common.confirm')"
|
||||||
@activate-b="handleCancel"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleConfirm"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 194 B |
Reference in New Issue
Block a user