Files
pihkaal-me/app/components/Settings/BottomScreen/NumberInput.vue

259 lines
6.1 KiB
Vue

<script setup lang="ts">
import gsap from "gsap";
const APP_COLOR_TO_FONT_COLOR: Record<string, string> = {
"#61829a": "#fbfbfb", // cyan
"#ba4900": "#fbe3d3", // maroon
"#fb0018": "#fbcbd3", // red
"#fb8afb": "#fbebfb", // pink
"#fb9200": "#fbf3e3", // orange
"#f3e300": "#fbfbdb", // yellow
"#aafb00": "#fbfbfb", // lime
"#00fb00": "#fbfbfb", // green
"#00a238": "#fbfbfb", // dark green
"#49db8a": "#ebfbf3", // duck
"#30baf3": "#fbfbfb", // light blue
"#0059f3": "#dbebfb", // blue
"#000092": "#ebebfb", // dark blue
"#8a00d3": "#f3dbfb", // purple
"#d300eb": "#fbfbfb", // magenta
"#fb0092": "#ebe3f3", // fuschia
};
const value = defineModel<number>({ required: true });
const props = withDefaults(
defineProps<{
x: number;
title: string;
digits?: 2 | 4;
min?: number;
max?: number;
disabled?: boolean;
selected?: boolean;
}>(),
{
digits: 2,
min: 0,
max: 99,
disabled: false,
selected: false,
},
);
const emit = defineEmits<{
select: [];
}>();
const app = useAppStore();
const { assets } = useAssets();
const { onRender, onClick } = useScreen();
const Y = 31;
const SQUARE_HEIGHT = 49;
const ARROW_IMAGE_HEIGHT =
assets.images.settings.bottomScreen.numberInputUp.rect.height;
const upImage = computed(() => {
if (props.digits === 2) {
return props.disabled
? assets.images.settings.bottomScreen.numberInputUpDisabled
: assets.images.settings.bottomScreen.numberInputUp;
} else {
return props.disabled
? assets.images.settings.bottomScreen.numberInputUpXlDisabled
: assets.images.settings.bottomScreen.numberInputUpXl;
}
});
const downImage = computed(() => {
if (props.digits === 2) {
return props.disabled
? assets.images.settings.bottomScreen.numberInputDownDisabled
: assets.images.settings.bottomScreen.numberInputDown;
} else {
return props.disabled
? assets.images.settings.bottomScreen.numberInputDownXlDisabled
: assets.images.settings.bottomScreen.numberInputDownXl;
}
});
const squareWidth = computed(() => upImage.value.rect.width);
const SLIDE_OFFSET = 96;
const SLIDE_DURATION = 0.25;
const ARROW_SLIDE_DELAY = 0.15;
const ARROW_SLIDE_DURATION = 0.167;
const isAnimating = ref(true);
const animation = reactive({
offsetY: SLIDE_OFFSET,
opacity: 0,
upArrowOffsetY: ARROW_IMAGE_HEIGHT,
downArrowOffsetY: -ARROW_IMAGE_HEIGHT,
});
const animateIntro = async () => {
isAnimating.value = true;
await gsap
.timeline()
.to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
.to(animation, { opacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
.to(
animation,
{ upArrowOffsetY: 0, duration: ARROW_SLIDE_DURATION, ease: "none" },
SLIDE_DURATION + ARROW_SLIDE_DELAY,
)
.to(
animation,
{ downArrowOffsetY: 0, duration: ARROW_SLIDE_DURATION, ease: "none" },
SLIDE_DURATION + ARROW_SLIDE_DELAY,
);
isAnimating.value = false;
};
const animateOutro = async () => {
isAnimating.value = true;
await gsap
.timeline()
.to(
animation,
{ offsetY: SLIDE_OFFSET, duration: SLIDE_DURATION, ease: "none" },
0,
)
.to(animation, { opacity: 0, duration: SLIDE_DURATION, ease: "none" }, 0);
};
defineExpose({ animateIntro, animateOutro });
const increase = () => {
const newValue = value.value + 1;
value.value = newValue > props.max ? props.min : newValue;
};
const decrease = () => {
const newValue = value.value - 1;
value.value = newValue < props.min ? props.max : newValue;
};
onRender((ctx) => {
ctx.globalAlpha = animation.opacity;
ctx.translate(0, animation.offsetY);
// arrow up (clipped to area above the number square)
ctx.save();
ctx.beginPath();
ctx.rect(props.x, 0, squareWidth.value, Y + ARROW_IMAGE_HEIGHT);
ctx.clip();
upImage.value.draw(ctx, props.x, Y + animation.upArrowOffsetY);
ctx.restore();
// outline
ctx.fillStyle = "#515151";
ctx.fillRect(
props.x,
Y + ARROW_IMAGE_HEIGHT,
squareWidth.value,
SQUARE_HEIGHT,
);
// background
ctx.fillStyle = props.selected ? app.color.hex : "#797979";
ctx.fillRect(
props.x + 1,
Y + ARROW_IMAGE_HEIGHT + 1,
squareWidth.value - 2,
SQUARE_HEIGHT - 2,
);
// value
ctx.font = "39px NDS39";
ctx.letterSpacing = "2px";
ctx.fillStyle = props.selected
? APP_COLOR_TO_FONT_COLOR[app.color.hex]!
: "#fbfbfb";
ctx.fillText(
value.value.toString().padStart(props.digits, "0"),
props.x + (props.digits === 2 ? 3 : 4),
Y + ARROW_IMAGE_HEIGHT + Math.floor((SQUARE_HEIGHT - 39) / 2) + 39,
);
// arrow down (clipped to area below the number square)
ctx.save();
ctx.beginPath();
ctx.rect(
props.x,
Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT,
squareWidth.value,
ARROW_IMAGE_HEIGHT,
);
ctx.clip();
downImage.value.draw(
ctx,
props.x,
Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT + animation.downArrowOffsetY,
);
ctx.restore();
// title
ctx.font = "10px NDS10";
ctx.fillStyle = "#000000";
ctx.letterSpacing = "0px";
fillTextHCentered(
ctx,
props.title,
props.x + 1,
Y + ARROW_IMAGE_HEIGHT * 2 + SQUARE_HEIGHT + 3 + 9,
downImage.value.rect.width,
);
}, 10);
useKeyDown(({ key }) => {
if (isAnimating.value || !props.selected || props.disabled) return;
switch (key) {
case "NDS_UP":
increase();
break;
case "NDS_DOWN":
decrease();
break;
}
});
onClick((x, y) => {
if (isAnimating.value || props.disabled) return;
if (
rectContains(
[props.x, Y, upImage.value.rect.width, ARROW_IMAGE_HEIGHT],
[x, y],
)
) {
increase();
emit("select");
} else if (
rectContains(
[props.x, Y + ARROW_IMAGE_HEIGHT, SQUARE_HEIGHT + 2, SQUARE_HEIGHT + 2],
[x, y],
)
) {
emit("select");
} else if (
rectContains(
[
props.x,
Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT + 2,
downImage.value.rect.width,
ARROW_IMAGE_HEIGHT,
],
[x, y],
)
) {
decrease();
emit("select");
}
});
defineOptions({ render: () => null });
</script>