Appearance
ScaleController
차트나 캔버스 요소의 확대/축소를 제어하는 UI 컨트롤러 컴포넌트입니다. 마우스 휠, 터치 제스처, 버튼 클릭을 통한 스케일 조정을 지원합니다.
인터랙티브 데모
오른쪽 버튼이나 데모 영역 위의 마우스 휠로 도형을 확대·축소하세요. 스케일 값은 호스트에서 0.5–2배로 제한합니다.
개요
ScaleController는 토폴로지 차트, 이미지 뷰어, 캔버스 기반 애플리케이션 등에서 사용할 수 있는 범용 스케일 컨트롤러입니다. 직관적인 UI와 다양한 입력 방식을 통해 사용자가 콘텐츠의 확대/축소를 쉽게 조절할 수 있습니다.
주요 특징
- 🎯 다양한 입력 지원: 마우스 휠, 터치 핀치, 버튼 클릭
- 📱 터치 최적화: 모바일 디바이스의 핀치 제스처 지원
- 🎨 위치 커스터마이징: 4가지 코너 위치 선택 가능
- ⚙️ 완전 커스터마이징: 크기, 마진, 아이콘 등 자유로운 스타일링
- ♿ 접근성: 키보드 네비게이션과 스크린 리더 지원
- 🔧 이벤트 기반: 유연한 스케일 로직 구현 가능
Props Interface
typescript
interface ScaleControllerProps {
/** 컨트롤러 위치 (기본값: 'bottom-right') */
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/** 컨트롤러 표시 여부 (기본값: true) */
visible?: boolean;
/** 마우스 휠 이벤트 활성화 여부 (기본값: true) */
enableWheel?: boolean;
/** 터치 이벤트 활성화 여부 (기본값: true) */
enableTouch?: boolean;
/** 버튼 크기 (기본값: 32px) */
buttonSize?: number;
/** 컨트롤러 마진 (기본값: 8px) */
margin?: number;
}Events
typescript
interface ScaleControllerEmits {
/** 마우스 휠 이벤트 (확대/축소) */
'wheel': [event: WheelEvent];
/** '+' 버튼 클릭 (확대) */
'clickUp': [];
/** '-' 버튼 클릭 (축소) */
'clickDown': [];
/** '원복' 버튼 클릭 (초기 스케일로 복원) */
'clickReset': [];
}기본 사용법
vue
<template>
<div class="chart-container">
<ScaleController
position="bottom-right"
:visible="true"
@wheel="handleWheel"
@click-up="handleZoomIn"
@click-down="handleZoomOut"
@click-reset="handleReset"
>
<!-- 스케일 대상 콘텐츠 -->
<canvas
ref="chartCanvas"
:width="canvasWidth"
:height="canvasHeight"
:style="{
transform: `scale(${scale})`,
transformOrigin: '0 0'
}"
/>
</ScaleController>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ScaleController } from '@jennifersoft/apm-components';
const scale = ref<number>(1);
const minScale = 0.1;
const maxScale = 5;
const scaleStep = 0.1;
const handleWheel = (event: WheelEvent) => {
const delta = event.deltaY > 0 ? -scaleStep : scaleStep;
updateScale(scale.value + delta);
};
const handleZoomIn = () => {
updateScale(scale.value + scaleStep);
};
const handleZoomOut = () => {
updateScale(scale.value - scaleStep);
};
const handleReset = () => {
updateScale(1);
};
const updateScale = (newScale: number) => {
scale.value = Math.max(minScale, Math.min(maxScale, newScale));
};
</script>SVG 차트와 함께 사용
vue
<template>
<div class="svg-chart-container">
<ScaleController
position="top-right"
:button-size="28"
:margin="12"
@wheel="handleWheel"
@click-up="zoomIn"
@click-down="zoomOut"
@click-reset="resetZoom"
>
<svg
ref="svgChart"
:width="chartWidth"
:height="chartHeight"
:viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`"
>
<!-- SVG 차트 내용 -->
<g v-for="node in nodes" :key="node.id">
<circle
:cx="node.x"
:cy="node.y"
:r="node.radius"
:fill="node.color"
/>
<text
:x="node.x"
:y="node.y"
text-anchor="middle"
dominant-baseline="middle"
>
{{ node.label }}
</text>
</g>
<g v-for="edge in edges" :key="edge.id">
<line
:x1="edge.source.x"
:y1="edge.source.y"
:x2="edge.target.x"
:y2="edge.target.y"
stroke="#999"
stroke-width="2"
/>
</g>
</svg>
</ScaleController>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
const chartWidth = 800;
const chartHeight = 600;
const viewBox = reactive({
x: 0,
y: 0,
width: chartWidth,
height: chartHeight
});
const baseViewBox = { ...viewBox };
const zoomLevel = ref<number>(1);
const handleWheel = (event: WheelEvent) => {
const zoomFactor = event.deltaY > 0 ? 1.1 : 0.9;
zoom(zoomFactor, event.offsetX, event.offsetY);
};
const zoomIn = () => {
zoom(0.9, chartWidth / 2, chartHeight / 2);
};
const zoomOut = () => {
zoom(1.1, chartWidth / 2, chartHeight / 2);
};
const resetZoom = () => {
Object.assign(viewBox, baseViewBox);
zoomLevel.value = 1;
};
const zoom = (factor: number, centerX: number, centerY: number) => {
const newWidth = viewBox.width * factor;
const newHeight = viewBox.height * factor;
// 줌 제한
if (newWidth > chartWidth * 10 || newWidth < chartWidth * 0.1) return;
const dx = (newWidth - viewBox.width) * (centerX / chartWidth);
const dy = (newHeight - viewBox.height) * (centerY / chartHeight);
viewBox.x -= dx;
viewBox.y -= dy;
viewBox.width = newWidth;
viewBox.height = newHeight;
zoomLevel.value = chartWidth / viewBox.width;
};
</script>토폴로지 차트와 통합
vue
<template>
<div class="topology-with-scale">
<ScaleController
position="bottom-left"
:enable-wheel="true"
:enable-touch="true"
@wheel="handleTopologyWheel"
@click-up="handleTopologyZoomIn"
@click-down="handleTopologyZoomOut"
@click-reset="handleTopologyReset"
>
<TopologyChart
ref="topologyRef"
:data="topologyData"
:width="800"
:height="600"
:enable-auto-scale="false"
/>
</ScaleController>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ScaleController } from '@jennifersoft/apm-components';
import { TopologyChart } from '@jennifersoft/apm-components';
const topologyRef = ref<InstanceType<typeof TopologyChart>>();
const handleTopologyWheel = (event: WheelEvent) => {
if (!topologyRef.value) return;
// TopologyChart의 내장 줌 기능 사용
const zoomEvent = new WheelEvent('wheel', {
deltaY: event.deltaY,
clientX: event.clientX,
clientY: event.clientY,
bubbles: true,
cancelable: true
});
topologyRef.value.$el.dispatchEvent(zoomEvent);
};
const handleTopologyZoomIn = () => {
// 프로그래밍 방식으로 줌 인
const wheelEvent = new WheelEvent('wheel', {
deltaY: -100,
bubbles: true,
cancelable: true
});
topologyRef.value?.$el.dispatchEvent(wheelEvent);
};
const handleTopologyZoomOut = () => {
// 프로그래밍 방식으로 줌 아웃
const wheelEvent = new WheelEvent('wheel', {
deltaY: 100,
bubbles: true,
cancelable: true
});
topologyRef.value?.$el.dispatchEvent(wheelEvent);
};
const handleTopologyReset = () => {
// 토폴로지 차트 리셋 (차트의 내장 메서드 사용)
topologyRef.value?.resetZoom();
};
</script>터치 제스처 처리
vue
<template>
<ScaleController
:enable-touch="true"
@wheel="handlePinchZoom"
>
<div
class="touch-area"
:style="{
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
transformOrigin: '0 0'
}"
>
<!-- 터치 대상 콘텐츠 -->
<img src="/large-image.jpg" alt="확대/축소 가능한 이미지" />
</div>
</ScaleController>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const scale = ref<number>(1);
const translateX = ref<number>(0);
const translateY = ref<number>(0);
const handlePinchZoom = (event: WheelEvent) => {
// 터치 핀치는 WheelEvent로 변환되어 전달됨
const scaleFactor = event.deltaY > 0 ? 0.95 : 1.05;
const newScale = scale.value * scaleFactor;
// 스케일 제한
if (newScale >= 0.5 && newScale <= 3) {
scale.value = newScale;
}
};
</script>커스텀 스타일링
vue
<template>
<ScaleController
position="top-left"
:button-size="40"
:margin="20"
class="custom-scale-controller"
>
<slot />
</ScaleController>
</template>
<style scoped>
.custom-scale-controller {
/* 커스텀 CSS 변수 오버라이드 */
--button-size: 40px;
--margin: 20px;
}
.custom-scale-controller :deep(.scale-controls__btn) {
/* 버튼 스타일 커스터마이징 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.custom-scale-controller :deep(.scale-controls__btn:hover) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.custom-scale-controller :deep(.scale-controls__btn:active) {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* 다크 테마 지원 */
@media (prefers-color-scheme: dark) {
.custom-scale-controller :deep(.scale-controls__btn) {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
border: 1px solid #4a5568;
}
}
</style>구성 요소
ScaleController는 다음 구성 요소들로 이루어져 있습니다:
버튼 종류
- 확대 버튼 (
+): 스케일 증가 - 원복 버튼 (⛶): 초기 스케일로 복원
- 축소 버튼 (
-): 스케일 감소
이벤트 처리
- 마우스 휠: 부드러운 확대/축소
- 터치 핀치: 모바일 디바이스 지원
- 버튼 클릭: 정확한 스케일 조정
설정 옵션
- 위치: 4개 코너 중 선택
- 크기: 버튼 크기와 마진 조정
- 활성화: 개별 이벤트 타입 제어
의존성
@jennifersoft/vue-components-v2: SvgIcon, ICON_TYPE- Vue 3: Composition API 기반
- 터치 이벤트 지원을 위한 모던 브라우저
브라우저 지원
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
- 모바일 브라우저 (iOS Safari, Android Chrome)
알려진 제한사항
- 터치 인터페이스: 복잡한 멀티터치 제스처는 제한적 지원
- 성능: 고해상도 캔버스에서 연속적인 스케일 변경 시 성능 저하 가능
- 브라우저 호환성: 일부 오래된 브라우저에서 터치 이벤트 지원 제한
활용 사례
- 토폴로지 차트: 네트워크 다이어그램 확대/축소
- 이미지 뷰어: 고해상도 이미지 상세 보기
- 캔버스 에디터: 그래픽 편집 도구의 줌 기능
- 데이터 시각화: 복잡한 차트의 상세 분석
- 지도 애플리케이션: 지도 영역 확대/축소
- 게임 인터페이스: 미니맵이나 전략 뷰어