mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-01 04:42:58 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			253 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|     <div :id="`scroll-div-${id}`" class="scroll-main">
 | |
|         <div class="main-div">
 | |
|             <div :id="`content-${id}`" class="content">
 | |
|                 <slot></slot>
 | |
|             </div>
 | |
|         </div>
 | |
|         <canvas :id="`scroll-${id}`" class="scroll"></canvas>
 | |
|     </div>
 | |
| </template>
 | |
| 
 | |
| <script lang="ts" setup>
 | |
| import { sleep } from 'mutate-animate';
 | |
| import { onMounted, onUnmounted, onUpdated } from 'vue';
 | |
| import { cancelGlobalDrag, useDrag, useWheel } from '../plugin/use';
 | |
| 
 | |
| let main: HTMLDivElement;
 | |
| 
 | |
| const props = defineProps<{
 | |
|     now?: number;
 | |
|     type?: 'vertical' | 'horizontal';
 | |
|     drag?: boolean;
 | |
|     width?: number;
 | |
|     update?: boolean;
 | |
|     noScroll?: boolean;
 | |
| }>();
 | |
| 
 | |
| const emits = defineEmits<{
 | |
|     (e: 'update:now', value: number): void;
 | |
|     (e: 'update:drag', value: boolean): void;
 | |
|     (e: 'update:update'): void;
 | |
| }>();
 | |
| 
 | |
| let now = 0;
 | |
| let total = 0;
 | |
| 
 | |
| const id = (1e8 * Math.random()).toFixed(0);
 | |
| const scale = window.devicePixelRatio;
 | |
| const width = props.width ?? 20;
 | |
| 
 | |
| const cssTarget = props.type === 'horizontal' ? 'left' : 'top';
 | |
| const canvasAttr = props.type === 'horizontal' ? 'width' : 'height';
 | |
| 
 | |
| let ctx: CanvasRenderingContext2D;
 | |
| let content: HTMLDivElement;
 | |
| let fromSelf = false;
 | |
| 
 | |
| const resize = async () => {
 | |
|     await calHeight(false);
 | |
|     draw();
 | |
| };
 | |
| 
 | |
| /** 绘制 */
 | |
| function draw() {
 | |
|     if (total === 0) return;
 | |
|     if (total < ctx.canvas[canvasAttr] / scale) {
 | |
|         now = 0;
 | |
|     } else if (now > total - ctx.canvas[canvasAttr] / scale) {
 | |
|         now = total - ctx.canvas[canvasAttr] / scale;
 | |
|     } else if (now < 0) {
 | |
|         now = 0;
 | |
|     }
 | |
|     if (props.noScroll) return;
 | |
|     const w = ctx.canvas.width;
 | |
|     const h = ctx.canvas.height;
 | |
|     emits('update:now', now);
 | |
|     const length =
 | |
|         Math.min(ctx.canvas[canvasAttr] / total / scale, 1) *
 | |
|         ctx.canvas[canvasAttr];
 | |
|     const py = (now / total) * ctx.canvas[canvasAttr];
 | |
| 
 | |
|     ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
 | |
|     ctx.beginPath();
 | |
|     if (props.type === 'horizontal') {
 | |
|         ctx.moveTo(Math.max(py + 5, 5), h / 2);
 | |
|         ctx.lineTo(Math.min(py + length - 5, ctx.canvas.width - 5), h / 2);
 | |
|     } else {
 | |
|         ctx.moveTo(w / 2, Math.max(py + 5, 5));
 | |
|         ctx.lineTo(w / 2, Math.min(py + length - 5, ctx.canvas.height - 5));
 | |
|     }
 | |
|     ctx.lineCap = 'round';
 | |
|     ctx.lineWidth = 6;
 | |
|     ctx.strokeStyle = '#fff';
 | |
|     ctx.stroke();
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 计算元素总长度
 | |
|  */
 | |
| async function calHeight(first: boolean = false) {
 | |
|     if (!first) {
 | |
|         await sleep(20);
 | |
|     }
 | |
|     const canvas = ctx.canvas;
 | |
|     const style2 = getComputedStyle(canvas);
 | |
|     canvas.style.width = `${width}px`;
 | |
|     canvas.width = width * scale;
 | |
|     canvas.height = parseFloat(style2.height) * scale;
 | |
|     if (props.noScroll) canvas.style.width = `0px`;
 | |
| 
 | |
|     if (props.type === 'horizontal') {
 | |
|         main.style.flexDirection = 'column';
 | |
|         canvas.style.height = `${width}px`;
 | |
|         canvas.style.width = '98%';
 | |
|         canvas.style.margin = '0 1% 0 1%';
 | |
|         canvas.width = parseFloat(style2.width) * scale;
 | |
|         canvas.height = width * scale;
 | |
|         if (props.noScroll) canvas.style.height = `0px`;
 | |
|     }
 | |
|     await new Promise(res => {
 | |
|         requestAnimationFrame(() => {
 | |
|             const style = getComputedStyle(content);
 | |
|             total = parseFloat(style[canvasAttr]);
 | |
|             res('');
 | |
|         });
 | |
|     });
 | |
| }
 | |
| 
 | |
| function scroll() {
 | |
|     draw();
 | |
|     content.style[cssTarget] = `${-now}px`;
 | |
| }
 | |
| 
 | |
| onUpdated(async () => {
 | |
|     if (fromSelf) return;
 | |
|     now = props.now ?? now;
 | |
|     content.style.transition = `${cssTarget} 0.2s ease-out`;
 | |
|     await calHeight(false);
 | |
|     scroll();
 | |
| });
 | |
| 
 | |
| let last: number;
 | |
| let contentLast: number;
 | |
| 
 | |
| function canvasDrag(x: number, y: number) {
 | |
|     emits('update:drag', true);
 | |
|     const d = props.type === 'horizontal' ? x : y;
 | |
|     const dy = d - last;
 | |
|     last = d;
 | |
|     if (ctx.canvas[canvasAttr] < total * scale)
 | |
|         now += ((dy * total) / ctx.canvas[canvasAttr]) * scale;
 | |
|     scroll();
 | |
| }
 | |
| 
 | |
| function contentDrag(x: number, y: number) {
 | |
|     emits('update:drag', true);
 | |
|     const d = props.type === 'horizontal' ? x : y;
 | |
|     const dy = d - contentLast;
 | |
|     contentLast = d;
 | |
|     if (ctx.canvas[canvasAttr] < total * scale) now -= dy;
 | |
|     scroll();
 | |
| }
 | |
| 
 | |
| onMounted(async () => {
 | |
|     main = document.getElementById(`scroll-div-${id}`) as HTMLDivElement;
 | |
|     const d = document.getElementById(`content-${id}`) as HTMLDivElement;
 | |
|     content = d;
 | |
|     const canvas = document.getElementById(`scroll-${id}`) as HTMLCanvasElement;
 | |
|     ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
 | |
| 
 | |
|     if (!props.noScroll) {
 | |
|         // 绑定滚动条拖拽事件
 | |
|         useDrag(
 | |
|             canvas,
 | |
|             canvasDrag,
 | |
|             (x, y) => {
 | |
|                 fromSelf = true;
 | |
|                 last = props.type === 'horizontal' ? x : y;
 | |
|                 content.style.transition = '';
 | |
|             },
 | |
|             () => {
 | |
|                 setTimeout(() => emits('update:drag', false));
 | |
|                 fromSelf = false;
 | |
|             },
 | |
|             true
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     // 绑定文本拖拽事件
 | |
|     useDrag(
 | |
|         content,
 | |
|         contentDrag,
 | |
|         (x, y) => {
 | |
|             fromSelf = true;
 | |
|             contentLast = props.type === 'horizontal' ? x : y;
 | |
|             content.style.transition = '';
 | |
|         },
 | |
|         () => {
 | |
|             setTimeout(() => emits('update:drag', false));
 | |
|             fromSelf = false;
 | |
|         },
 | |
|         true
 | |
|     );
 | |
| 
 | |
|     useWheel(content, (x, y) => {
 | |
|         fromSelf = true;
 | |
|         const d = x !== 0 ? x : y;
 | |
|         if (Math.abs(d) > 30) {
 | |
|             content.style.transition = `${cssTarget} 0.2s ease-out`;
 | |
|         } else {
 | |
|             content.style.transition = '';
 | |
|         }
 | |
|         now += d;
 | |
|         scroll();
 | |
|         fromSelf = false;
 | |
|     });
 | |
| 
 | |
|     window.addEventListener('resize', resize);
 | |
|     await calHeight(true);
 | |
|     await calHeight();
 | |
|     draw();
 | |
| });
 | |
| 
 | |
| onUnmounted(() => {
 | |
|     window.removeEventListener('resize', resize);
 | |
|     cancelGlobalDrag(canvasDrag);
 | |
|     cancelGlobalDrag(contentDrag);
 | |
| });
 | |
| </script>
 | |
| 
 | |
| <style lang="less" scoped>
 | |
| .scroll {
 | |
|     opacity: 0.2;
 | |
|     transition: opacity 0.2s linear;
 | |
| }
 | |
| 
 | |
| .scroll:hover {
 | |
|     opacity: 0.4;
 | |
| }
 | |
| 
 | |
| .scroll:active {
 | |
|     opacity: 0.6;
 | |
| }
 | |
| 
 | |
| .scroll-main {
 | |
|     display: flex;
 | |
|     flex-direction: row;
 | |
|     max-width: 100%;
 | |
|     max-height: 100%;
 | |
|     justify-content: stretch;
 | |
| }
 | |
| 
 | |
| .content {
 | |
|     width: 100%;
 | |
|     position: relative;
 | |
| }
 | |
| 
 | |
| .main-div {
 | |
|     flex-basis: 100%;
 | |
|     overflow: hidden;
 | |
| }
 | |
| </style>
 |