๐ŸŒŠย Scatter ํšจ๊ณผ

Canvas API๋ฅผ ์ด์šฉํ•˜์—ฌ ํ…์ŠคํŠธ๊ฐ€ ๋งˆ์šฐ์Šค์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ฉฐ, ๋ชจ๋ž˜์ฒ˜๋Ÿผ ํฉ์–ด์ง€๋Š” ํšจ๊ณผ์ž…๋‹ˆ๋‹ค.

IntroPage.tsx ์™€ MainPage.tsx์—์„œ ์‚ฌ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ์žฌ์‚ฌ์šฉ์„ ์œ„ํ•ด ScatterCanvas.tsx ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

ezgif-1-f75bf0778e.gif

ScatterCanvas.tsx Props ์„ค๋ช…

์ „์ฒด์ ์ธ ๋™์ž‘ ์„ค๋ช…

*** pointerDiv - ์ปจ๋ฒ„์Šค ์œ„์—(๋” ๋†’์€ z-index) ์œ„์น˜ํ•œ Div Element๋กœ, ๊ทธ๋ ค์ง„ text์™€ ๊ฐ™์€ ๋„ˆ๋น„, ๋†’์ด๋ฅผ ๊ฐ€์ง€๊ณ  text์™€ ๊ฐ™์€ ์œ„์น˜์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. Scatter ํšจ๊ณผ์˜ ์ง„ํ–‰์„ ์œ„ํ•ด์„œ ์‚ฌ์šฉ์ž์˜ ๋งˆ์šฐ์Šค์ด๋ฒคํŠธ(onMouseUp, onMouseDown)์„ ๋ฐ›๊ณ , ์‚ฌ์šฉ์ž์˜ ๋งˆ์šฐ์Šค ํด๋ฆญ์„ ์œ ๋„ํ•˜๊ธฐ ์œ„ํ•ด cursor: pointer ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

1๏ธโƒฃ canvas์— ์›ํ•˜๋Š” ํฐํŠธ๋กœ text ๊ทธ๋ฆฌ๊ธฐ ๋ฐ pointerDiv ๋„ˆ๋น„์™€ ๋†’์ด ์„ค์ •

fillText๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ์›ํ•˜๋Š” ํฐํŠธ๋กœ ์ปจ๋ฒ„์Šค์— text๋ฅผ ๊ทธ๋ ค์ค๋‹ˆ๋‹ค.

์ปจ๋ฒ„์Šค์—๋Š” cssํŒŒ์ผ์— ์ž‘์„ฑํ•ด์ค€ mediaQuery font-size๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ปจ๋ฒ„์Šค์˜ ๋„ˆ๋น„์— ๋”ฐ๋ผ ํฐํŠธ ์‚ฌ์ด์ฆˆ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ๊ฐ์ฒด(canvasFontSize)๋ฅผ ๋ณ„๋„๋กœ ์ž‘์„ฑํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์€ ์‚ฌ์šฉํ•˜๋ คํ•˜๋Š” ํฐํŠธ์˜ load๊ฐ€ ์™„๋ฃŒ๋œ ์ดํ›„์— text๋ฅผ ๊ทธ๋ ค์ฃผ์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. Web API ์ค‘ CSS Font Loading API๋ฅผ ์ด์šฉํ•˜์—ฌ ํฐํŠธํŒŒ์ผ์˜ ๋กœ๋”ฉ์ด ์™„๋ฃŒ๋œ ์ดํ›„์— text๋ฅผ ๊ทธ๋ ค์ค๋‹ˆ๋‹ค.

text๋ฅผ ๊ทธ๋ฆฐ ํ›„ pointerDiv์˜ ๋„ˆ๋น„์™€ ๋†’์ด๋ฅผ ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค. ๋†’์ด๋Š” text ๋ฐฐ์—ด์˜ ๊ธธ์ด๋ฅผ ํ†ตํ•ด ๊ณ„์‚ฐํ•ด์ค€ totalHeight ๊ฐ’์ด ๋˜๊ณ , ๋„ˆ๋น„๋Š” text ๋ฐฐ์—ด์ค‘ ๊ฐ€์žฅ ๊ธด text์˜ ๋„ˆ๋น„๊ฐ’์ด ๋ฉ๋‹ˆ๋‹ค. ๋„ˆ๋น„๊ฐ’์€ ์ปจ๋ฒ„์Šค์˜ measureText๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

const drawText = useCallback(() => {
    if (!canvas) return;

    const FONT_SIZE = canvasFontSize(canvas.CANVAS_WIDTH);
    const LINE_VALUE = 5; // ์ค„ ๊ฐ„๊ฒฉ ๊ฐ’ 5px
    const ctx = canvas.ctx as CanvasRenderingContext2D;
    const totalHeight = text.length * FONT_SIZE + (LINE_VALUE * text.length - 1);
    const x = canvas.CANVAS_WIDTH / 2;
    const y = (canvas.CANVAS_HEIGHT - totalHeight) / 2 + FONT_SIZE;

    let met;
    let maxWidth = 0;
    let lineHeight = 0;

    const fontFile = new FontFace("KoPubWorld Medium", `url(${font_ttf}) format("truetype")`);
    document.fonts.add(fontFile);
    fontFile
      .load()
      .then(() => {
        ctx.font = `${FONT_SIZE}px KoPubWorld Medium`;
        ctx.fillStyle = "#FFFFFF";
        ctx.textAlign = "center";

        for (let i = 0; i < text.length; i++) {
          ctx.fillText(text[i], x, y + FONT_SIZE * i + lineHeight);
          lineHeight += LINE_VALUE;

          met = ctx.measureText(text[i]);
          if (!maxWidth) maxWidth = met.width;
          else if (maxWidth < met.width) maxWidth = met.width;
        }

        if (speechService.tts)
          speechService.synth.speak(text.join(""), {
            endEvent: () => {
              onMouseDownPointerDiv({ width: maxWidth, height: totalHeight });
              onMouseUpPointerDiv();
            },
          });
        else setPointerDiv({ width: maxWidth, height: totalHeight });
      })
      .catch((err: string) => {
        console.log(`font ์—๋Ÿฌ ๋ฐœ์ƒ: ${err}`);
      });
  }, [canvas, text, speechService, onMouseDownPointerDiv, onMouseUpPointerDiv]);

useEffect(() => {
    if (!canvas) return;
    canvas.init();
    canvas.setFrame(15);

    drawText();

    const myResize = debounce(() => {
			**// resize ์ด๋ฒคํŠธ๋Š” animation ์‹คํ–‰์ค‘์—๋Š” ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.**
			if (canvas.isAnim) return;

      canvas.init();
      drawText();
    }, 300);

    window.addEventListener("resize", myResize);

    return () => {
      window.removeEventListener("resize", myResize);
    };
  }, [canvas, drawText]);