Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions libs/leon-sans-react/src/domain/WreathSansController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import gsap, { Power0, Power3 } from 'gsap';
import { Align, CHARSET, ModelData } from 'leonsans';
import { Align, CHARSET, ModelData, Rect } from 'leonsans';
import LeonSans from 'leonsans';
import * as PIXI from 'pixi.js';

Expand Down Expand Up @@ -42,6 +42,7 @@ type LeonOptions = {
size?: number;
weight?: number;
pathGap?: number;
align?: Align;
};

type WreathSansProps = {
Expand Down Expand Up @@ -99,14 +100,16 @@ export default class WreathSansController {
}: WreathSansProps) {
const canvasWidth = canvas?.clientWidth ?? 300;
const canvasHeight = canvas?.clientHeight ?? 300;
const initialSize = dynamicSize ? (leonOptions?.size ?? 130) : 0;

this.leon = new LeonSans({
text: initialText,
color: [leonOptions?.color ?? '#704234'],
size: 0,
size: initialSize,
weight: leonOptions?.weight ?? 400,
isPattern: true,
pathGap: leonOptions?.pathGap ?? 1 / 20,
align: leonOptions?.align ?? 'left',
});

this.leon.update();
Expand Down Expand Up @@ -178,6 +181,7 @@ export default class WreathSansController {

set align(align: Align) {
this.leon.align = align;
this.leon.updateDrawingPaths();
this.updatePositions();
}

Expand Down Expand Up @@ -340,6 +344,15 @@ export default class WreathSansController {
});
}

getTextRect(): Rect {
return {
x: this.leon.rect.x,
y: this.leon.rect.y,
w: this.leon.rect.w,
h: this.leon.rect.h,
};
}

/**
* leonsans의 데이터를 사용하여 글자들의 위치를 업데이트한다.
* 그러나 leonsans에는 영향을 주지 않는다.
Expand Down Expand Up @@ -373,7 +386,7 @@ export default class WreathSansController {
private dynamicResize() {
if (this.leon.rect.w === 0) return;
const newSize = this.size * (this.canvas.clientWidth / this.leon.rect.w);
if (newSize >= this._minSize) this.size = newSize;
this.size = Math.max(newSize, this._minSize);
}

private async loadAssets() {
Expand Down
21 changes: 20 additions & 1 deletion libs/leon-sans-react/src/hooks/createWreathSans.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CHARSET } from 'leonsans';
import { Align, CHARSET } from 'leonsans';
import { ComponentPropsWithoutRef } from 'react';

import WreathSansController from '../domain/WreathSansController';
Expand All @@ -9,35 +9,44 @@ type Props = {
color?: string;
size?: number;
weight?: number;
align?: Align;
// canvas config
width?: number;
height?: number;
pixelRatio?: number;
background?: string;
backgroundAlpha?: number;
darkMode?: boolean;
fitToWidth?: boolean;
minSize?: number;
};

export default function createWreathSans({
initialText,
color = '#000000',
size = 60,
weight = 400,
align = 'left',
width = 800,
height = 600,
background = '#ffffff',
backgroundAlpha = 1,
darkMode,
fitToWidth = false,
minSize,
}: Props) {
const wreathSansController = new WreathSansController({
initialText,
background,
backgroundAlpha,
darkMode,
dynamicSize: fitToWidth,
minSize,
leonOptions: {
color,
size,
weight,
align,
},
});

Expand Down Expand Up @@ -134,11 +143,21 @@ export default function createWreathSans({
return wreathSansController.leon.text;
}

function setAlign(newAlign: Align) {
wreathSansController.align = newAlign;
}

function getTextRect() {
return wreathSansController.getTextRect();
}

return {
WreathSansCanvas,
onInputHandler,
resize,
redraw,
getText,
setAlign,
getTextRect,
};
}
26 changes: 22 additions & 4 deletions projects/waffle-sans/src/components/Letter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,27 @@ type LetterProps = {
content: string;
sans: string;
mode: string;
align: string;
};

export default function Letter({ sender, content, sans, mode }: LetterProps) {
export default function Letter({
sender,
content,
sans,
mode,
align,
}: LetterProps) {
const [stage, setStage] = useState<(typeof stages)[number]>('shake');
const [contentHeight, setContentHeight] = useState<number | null>(null);
const parsedMode = useMemo(
() => (mode === 'o' ? 'outside' : 'inside'),
[mode],
);
const baseHeight = stage === 'out' ? 652 : 252;
const adjustedHeight =
stage === 'out' && contentHeight
? Math.max(baseHeight, contentHeight)
: baseHeight;

const onClickLetter = useCallback(() => {
const currentStageIndex = stages.findIndex((s) => s === stage);
Expand All @@ -33,7 +46,7 @@ export default function Letter({ sender, content, sans, mode }: LetterProps) {
$isOutside={parsedMode === 'outside'}
$isOut={stage === 'out'}
/>
<Container onClick={onClickLetter} $stage={stage}>
<Container onClick={onClickLetter} $stage={stage} $height={adjustedHeight}>
<LetterBack
$isOutside={parsedMode === 'outside'}
$isOut={stage === 'out'}
Expand All @@ -45,6 +58,8 @@ export default function Letter({ sender, content, sans, mode }: LetterProps) {
content={content}
mode={mode}
stage={stage}
align={align}
onHeightChange={setContentHeight}
/>
</PaperWrapper>
<LetterFront
Expand Down Expand Up @@ -84,10 +99,13 @@ export default function Letter({ sender, content, sans, mode }: LetterProps) {
);
}

const Container = styled.div<{ $stage: 'shake' | 'close' | 'open' | 'out' }>`
const Container = styled.div<{
$stage: 'shake' | 'close' | 'open' | 'out';
$height: number;
}>`
position: relative;
width: 100%;
height: ${({ $stage }) => ($stage === 'out' ? '652px' : '252px')};
height: ${({ $height }) => `${$height}px`};

transition: 1s ease;

Expand Down
5 changes: 5 additions & 0 deletions projects/waffle-sans/src/components/PostPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ export default function PostPreview({ mode = Mode.OUTSIDE }: Props) {
const value = useRecoilValue(postFormState);
const url = useMemo(() => new URL(window.location.href), []);
const sans = useMemo(() => decoder(url, 'sans'), [url]);
const align = useMemo(() => decoder(url, 'align'), [url]);
const { ref, WreathSansCanvas } = useWreathSans({
initialText: sans,
darkMode: mode === Mode.OUTSIDE,
fontColor: mode === Mode.OUTSIDE ? '#704234' : '#B27E41',
align:
align === 'left' || align === 'center' || align === 'right'
? align
: undefined,
});

return (
Expand Down
115 changes: 92 additions & 23 deletions projects/waffle-sans/src/components/ReceivedContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';

import useWreathSans from '../hooks/useWreathSans';
Expand All @@ -8,7 +8,9 @@ type ReceivedContentProps = {
content: string;
sans: string;
mode: string;
stage?: string;
stage: string;
align: string;
onHeightChange: (height: number) => void;
};

export default function ReceivedContent({
Expand All @@ -17,33 +19,98 @@ export default function ReceivedContent({
sans,
mode,
stage,
align,
onHeightChange,
}: ReceivedContentProps) {
const [width, setWidth] = useState(280);
const [height, setHeight] = useState(196);
const [baseHeight, setBaseHeight] = useState<number | null>(null);
// const [containerHeight, setContainerHeight] = useState<number | null>(null);
const [sansHeight, setSansHeight] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const baseSansHeight = useMemo(
() => (baseHeight ? baseHeight * 0.3 : null),
[baseHeight],
);

const { ref, WreathSansCanvas, redraw } = useWreathSans({
const { ref, WreathSansCanvas, redraw, resize, getTextRect } = useWreathSans({
width,
height,
initialText: sans,
darkMode: mode === 'o',
fontColor: mode === 'o' ? '#704234' : '#B27E41',
fitToWidth: true,
minSize: 0,
align:
align === 'left' || align === 'center' || align === 'right'
? align
: undefined,
});

useEffect(() => {
if (ref?.current) {
setWidth(ref?.current?.offsetWidth);
setHeight(ref.current.offsetHeight);
redraw();
}
}, [redraw, ref]);
const updateBaseSize = () => {
if (!containerRef.current) return;
const containerWidth = containerRef.current.offsetWidth;
setBaseHeight(containerWidth / 0.55);
if (ref.current) {
const wrapperWidth = ref.current.offsetWidth;
setWidth(wrapperWidth);
}
};
updateBaseSize();
window.addEventListener('resize', updateBaseSize);
return () => {
window.removeEventListener('resize', updateBaseSize);
};
}, []);

useEffect(() => {
if (!baseHeight) return;
const updateSansHeight = () => {
const rect = getTextRect();
if (!rect || rect.h === 0) return;
const targetHeight = Math.ceil(rect.h + 12);
const minHeight = baseSansHeight ?? 0;
setSansHeight(Math.max(targetHeight, minHeight));
};
const rafId = requestAnimationFrame(updateSansHeight);
return () => cancelAnimationFrame(rafId);
}, [baseHeight, baseSansHeight, getTextRect, sans, stage]);

// TODO: 필요한가?
// useEffect(() => {
// if (!baseHeight || !baseSansHeight) return;
// const nextSansHeight = sansHeight ?? baseSansHeight;
// const delta = Math.max(0, nextSansHeight - baseSansHeight);
// const nextHeight = Math.ceil(baseHeight + delta);
// setContainerHeight(nextHeight);
// onHeightChange?.(nextHeight);
// }, [baseHeight, baseSansHeight, onHeightChange, sansHeight]);

useEffect(() => {
if (!containerRef.current) return;
onHeightChange(containerRef.current.offsetHeight);
}, [sansHeight, containerRef, onHeightChange]);


useEffect(() => {
if (!sansHeight || !width) return;
setHeight(sansHeight);
resize(width, sansHeight);
redraw();
}, [redraw, resize, sansHeight, width]);

useEffect(() => {
redraw();
}, [redraw, stage]);

return (
<Container $isOutside={mode === 'o'}>
<SansWrapper ref={ref}>
<Container
ref={containerRef}
$isOutside={mode === 'o'}
// $height={containerHeight}
>
<SansWrapper ref={ref} $height={sansHeight}>
<WreathSansCanvas />
</SansWrapper>
<MainText $isOutside={mode === 'o'}>{content}</MainText>
Expand All @@ -53,40 +120,42 @@ export default function ReceivedContent({
);
}

const Container = styled.div<{ $isOutside: boolean }>`
const OUTSIDE_LETTER_BG = `${import.meta.env.BASE_URL}background_outside_letter.png`;
const INSIDE_LETTER_BG = `${import.meta.env.BASE_URL}background_inside_letter.png`;

const Container = styled.div<{ $isOutside: boolean; $height?: number | null }>`
position: relative;
top: 10px;
width: 100%;
aspect-ratio: 0.55;
/* TODO: auto로 두면 안 되는 이유? */
/* height: ${({ $height }) => ($height ? `${$height}px` : 'auto')};
aspect-ratio: ${({ $height }) => ($height ? 'auto' : '0.55')}; */
box-sizing: border-box;

padding: 45px 40px;
padding: 45px 40px 80px 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 3%;
gap: 10px;

background-image: url(${({ $isOutside }) =>
$isOutside
? `${import.meta.env.BASE_URL}background_outside_letter.png`
: `${import.meta.env.BASE_URL}background_inside_letter.png`});
background-image: url(${({ $isOutside }) => $isOutside ? OUTSIDE_LETTER_BG : INSIDE_LETTER_BG});
background-size: cover;
background-position: bottom center;
box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.15);
`;

const SansWrapper = styled.div`
const SansWrapper = styled.div<{ $height: number | null }>`
width: 100%;
height: 30%;
height: ${({ $height }) => ($height ? `${$height}px` : '30%')};
`;

const MainText = styled.div<{ $isOutside: boolean }>`
const MainText = styled.pre<{ $isOutside: boolean }>`
color: ${({ $isOutside }) => ($isOutside ? `#315c57` : `#FEDCB4`)};

width: 100%;
height: 30%;
flex-shrink: 0;
overflow-y: auto;
white-space: pre-wrap;

text-align: justify;
font-family: Inter;
Expand Down
Loading