Files
tablejerseys-web/app/components/designer/DesignerCanvas.vue
Frank John Begornia 3ba0b250ed
Some checks failed
Deploy Production / deploy (push) Has been cancelled
first commit
2026-01-12 22:16:36 +08:00

152 lines
3.8 KiB
Vue

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import type { Canvas as FabricCanvas } from "fabric";
const props = defineProps<{
size: number;
registerCanvas: (payload: {
canvas: FabricCanvas;
fabric: typeof import("fabric");
}) => void;
unregisterCanvas: () => void;
backgroundColor: string;
}>();
const canvasElement = ref<HTMLCanvasElement | null>(null);
const containerElement = ref<HTMLDivElement | null>(null);
const isReady = ref(false);
let fabricCanvas: FabricCanvas | null = null;
let fabricModule: typeof import("fabric") | null = null;
let resizeObserver: ResizeObserver | null = null;
const syncCanvasDomStyles = () => {
if (!fabricCanvas) {
return;
}
const canvases = [fabricCanvas.lowerCanvasEl, fabricCanvas.upperCanvasEl];
canvases.forEach((element) => {
if (!element) {
return;
}
element.classList.add("rounded-full");
element.style.borderRadius = "9999px";
element.style.backgroundColor = "transparent";
});
};
const updateCssDimensions = (dimension?: number) => {
if (!fabricCanvas) {
return;
}
const targetSize =
dimension ??
containerElement.value?.clientWidth ??
containerElement.value?.clientHeight ??
null;
if (!targetSize) {
return;
}
fabricCanvas.setDimensions(
{
width: targetSize,
height: targetSize,
},
{ cssOnly: true }
);
fabricCanvas.calcOffset();
fabricCanvas.requestRenderAll();
syncCanvasDomStyles();
};
const observeContainer = () => {
if (!containerElement.value || !fabricCanvas) {
return;
}
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
const dimension = Math.min(entry.contentRect.width, entry.contentRect.height);
updateCssDimensions(dimension);
});
resizeObserver.observe(containerElement.value);
updateCssDimensions();
};
const setupCanvas = async () => {
if (typeof window === "undefined" || !canvasElement.value) {
return;
}
fabricModule = await import("fabric");
const { Canvas } = fabricModule;
fabricCanvas = new Canvas(canvasElement.value, {
backgroundColor: "transparent",
selection: true,
preserveObjectStacking: true,
});
fabricCanvas.setDimensions({ width: props.size, height: props.size });
props.registerCanvas({ canvas: fabricCanvas, fabric: fabricModule });
observeContainer();
syncCanvasDomStyles();
isReady.value = true;
};
onMounted(() => {
setupCanvas();
});
onBeforeUnmount(() => {
props.unregisterCanvas();
resizeObserver?.disconnect();
resizeObserver = null;
fabricCanvas = null;
fabricModule = null;
isReady.value = false;
});
watch(
() => props.size,
(next, prev) => {
if (!isReady.value || next === prev || !fabricCanvas) {
return;
}
fabricCanvas.setDimensions({ width: next, height: next });
updateCssDimensions();
fabricCanvas.renderAll();
}
);
</script>
<template>
<div
class="relative mx-auto flex max-w-[min(720px,100%)] items-center justify-center overflow-hidden rounded-3xl border border-slate-700/60 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/40"
>
<div class="relative aspect-square w-full">
<div class="absolute inset-4 sm:inset-5 md:inset-6 lg:inset-8">
<div ref="containerElement" class="relative h-full w-full">
<canvas
ref="canvasElement"
class="absolute inset-0 h-full w-full rounded-full"
:width="size"
:height="size"
/>
<div
v-if="!isReady"
class="absolute inset-0 grid place-items-center rounded-full bg-slate-900/70"
>
<span class="text-sm font-medium text-slate-400">Loading canvas</span>
</div>
</div>
</div>
</div>
</div>
</template>