|
| 1 | +<script setup lang="ts"> |
| 2 | +import { onMounted, reactive, ref, watch } from 'vue' |
| 3 | +import { gsap } from 'gsap' |
| 4 | +import { Draggable } from 'gsap/Draggable' |
| 5 | +import { useElementBounding, useElementSize, useMouse, useWindowSize } from '@vueuse/core' |
| 6 | +import type { UseMouseEventExtractor } from '@vueuse/core' |
| 7 | +import { Html } from '../../index' |
| 8 | +
|
| 9 | +const emit = defineEmits(['updateTransform']) |
| 10 | +
|
| 11 | +let dragTransform, dragScaleRotation |
| 12 | +
|
| 13 | +gsap.registerPlugin(Draggable) |
| 14 | +
|
| 15 | +const container = ref<HTMLElement | null>(null) |
| 16 | +const outline = ref<HTMLElement | null>(null) |
| 17 | +const wrapper = ref<HTMLElement | null>(null) |
| 18 | +const handle = ref<HTMLElement | null>(null) |
| 19 | +const isVisible = ref(false) |
| 20 | +const resizerIsActive = ref(false) |
| 21 | +const isGrabbing = ref(false) |
| 22 | +const angle = ref(0) |
| 23 | +
|
| 24 | +const { width: windowWidth, height: windowHeight } = useWindowSize() |
| 25 | +
|
| 26 | +const { width: wrapperWidth, height: wrapperHeight } = useElementSize(wrapper) |
| 27 | +const { width, height, left, top } = useElementBounding(container) |
| 28 | +
|
| 29 | +const extractor: UseMouseEventExtractor = (event) => { |
| 30 | + if (event instanceof Touch) { return null } |
| 31 | +
|
| 32 | + const offsetX = event.clientX |
| 33 | + const offsetY = event.clientY |
| 34 | +
|
| 35 | + return [offsetX, offsetY] |
| 36 | +} |
| 37 | +
|
| 38 | +const { x: xWindow, y: yWindow } = useMouse({ type: extractor }) |
| 39 | +
|
| 40 | +const origSize = reactive({ width: 0, height: 0 }) |
| 41 | +const len1 = ref(0) |
| 42 | +const angle1 = ref(0) |
| 43 | +const scale = ref(1) |
| 44 | +
|
| 45 | +watch([xWindow, yWindow], () => { |
| 46 | + if (isGrabbing.value) { return } |
| 47 | +
|
| 48 | + const centerX = left.value |
| 49 | + const centerY = top.value |
| 50 | +
|
| 51 | + const distX = xWindow.value - centerX |
| 52 | + const distY = yWindow.value - centerY |
| 53 | + const distance = Math.sqrt(distX * distX + distY * distY) |
| 54 | +
|
| 55 | + const radius = Math.min(wrapperWidth.value, wrapperHeight.value) / 2 |
| 56 | + const distanceNormalized = distance / radius |
| 57 | +
|
| 58 | + resizerIsActive.value = distanceNormalized < 1.05 && distanceNormalized > 0.95 |
| 59 | +
|
| 60 | + const angleRad = Math.atan2(distY, distX) |
| 61 | + angle.value = angleRad + Math.PI / 2 |
| 62 | +}) |
| 63 | +
|
| 64 | +watch(resizerIsActive, () => { |
| 65 | + // if (resizerIsActive.value) { |
| 66 | + // dragTransform[0].disable() |
| 67 | + // dragScaleRotation[0].enable() |
| 68 | + // } |
| 69 | + // else { |
| 70 | + // dragTransform[0].enable() |
| 71 | + // dragScaleRotation[0].disable() |
| 72 | + // } |
| 73 | +}) |
| 74 | +
|
| 75 | +origSize.width = wrapperWidth.value |
| 76 | +origSize.height = wrapperHeight.value |
| 77 | +
|
| 78 | +const origVector = { |
| 79 | + x: 0, |
| 80 | + y: -origSize.height / 2, |
| 81 | +} |
| 82 | +
|
| 83 | +len1.value = Math.sqrt(origVector.x * origVector.x + origVector.y * origVector.y) |
| 84 | +
|
| 85 | +const v1 = { |
| 86 | + x: origVector.x / len1.value, |
| 87 | + y: origVector.y / len1.value, |
| 88 | +} |
| 89 | +
|
| 90 | +angle1.value = Math.atan2(v1.y, v1.x) |
| 91 | +
|
| 92 | +onMounted(() => { |
| 93 | + console.log(container.value, handle.value, outline.value, wrapper.value) |
| 94 | + gsap.set([container.value, handle.value, outline.value, wrapper.value], { |
| 95 | + xPercent: -50, |
| 96 | + yPercent: -50, |
| 97 | + }) |
| 98 | +
|
| 99 | + gsap.set(outline.value, { |
| 100 | + width: origSize.width, |
| 101 | + height: origSize.height, |
| 102 | + }) |
| 103 | +
|
| 104 | + gsap.set(handle.value, { |
| 105 | + x: origVector.x, |
| 106 | + y: origVector.y, |
| 107 | + rotation: angle1.value, |
| 108 | + }) |
| 109 | +
|
| 110 | + dragTransform = Draggable.create(container.value, { |
| 111 | + onDrag(this: Draggable) { |
| 112 | + updateTransformData(scale.value, angle.value, this.x, this.y) |
| 113 | + }, |
| 114 | + }) |
| 115 | +
|
| 116 | + dragScaleRotation = Draggable.create(handle.value, { |
| 117 | + onPress: stopPropagation, |
| 118 | + onRelease: () => { isGrabbing.value = false }, |
| 119 | + onDrag(this: Draggable) { |
| 120 | + transformItems(this) |
| 121 | + }, |
| 122 | + }) |
| 123 | +}) |
| 124 | +
|
| 125 | +function getCenteredPosition(x: number, y: number) { |
| 126 | + const centerX = windowWidth.value / 2 |
| 127 | + const centerY = windowHeight.value / 2 |
| 128 | +
|
| 129 | + return { |
| 130 | + x: x - centerX, |
| 131 | + y: y - centerY, |
| 132 | + } |
| 133 | +} |
| 134 | +
|
| 135 | +function updateTransformData(scaleValue: number, rotationValue: number, posX: number, posY: number) { |
| 136 | + emit('updateTransform', { |
| 137 | + scale: scaleValue, |
| 138 | + rotation: rotationValue, |
| 139 | + position: { x: posX, y: posY }, |
| 140 | + }) |
| 141 | +} |
| 142 | +
|
| 143 | +function transformItems(self: Draggable) { |
| 144 | + const v2 = { |
| 145 | + x: self.x, |
| 146 | + y: self.y, |
| 147 | + } |
| 148 | +
|
| 149 | + const len2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y) |
| 150 | +
|
| 151 | + if (len2) { |
| 152 | + v2.x /= len2 |
| 153 | + v2.y /= len2 |
| 154 | + } |
| 155 | +
|
| 156 | + const angle = Math.atan2(v2.y, v2.x) - angle1.value |
| 157 | +
|
| 158 | + scale.value = len2 / len1.value |
| 159 | +
|
| 160 | + gsap.set(outline.value, { |
| 161 | + width: origSize.width * scale.value, |
| 162 | + height: origSize.height * scale.value, |
| 163 | + rotation: `${angle}_rad`, |
| 164 | + }) |
| 165 | +
|
| 166 | + gsap.set(wrapper.value, { |
| 167 | + scale: scale.value, |
| 168 | + rotation: `${angle}_rad`, |
| 169 | + }) |
| 170 | +
|
| 171 | + gsap.set(handle.value, { |
| 172 | + // rotation: Math.atan2(-self.y, -self.x) * (180 / Math.PI) + 90, |
| 173 | + }) |
| 174 | +
|
| 175 | + const { x: posX, y: posY } = getCenteredPosition(left.value, top.value) |
| 176 | +
|
| 177 | + updateTransformData(scale.value, angle, posX, posY) |
| 178 | +} |
| 179 | +
|
| 180 | +function stopPropagation(event) { |
| 181 | + event.stopPropagation() |
| 182 | +
|
| 183 | + isGrabbing.value = true |
| 184 | + console.log('start drag') |
| 185 | +} |
| 186 | +
|
| 187 | +function updatePosition() { |
| 188 | + console.log('update position resizer') |
| 189 | + // const { x: posX, y: posY } = getCenteredPosition(left.value, top.value) |
| 190 | +
|
| 191 | + // updateTransformData(scale.value, angle.value, posX, posY) |
| 192 | +} |
| 193 | +
|
| 194 | +defineExpose({ updatePosition, instance: container }) |
| 195 | +</script> |
| 196 | + |
| 197 | +<template> |
| 198 | + <Html |
| 199 | + center |
| 200 | + > |
| 201 | + <div ref="container" class="container" :class="{ hide: !isVisible }"> |
| 202 | + <div ref="wrapper" class="wrapper"></div> |
| 203 | + <span ref="outline" class="outline">outline</span> |
| 204 | + <span class="dot-center"></span> |
| 205 | + <div ref="handle" class="handle">handle</div> |
| 206 | + </div> |
| 207 | + </Html> |
| 208 | +</template> |
| 209 | + |
| 210 | +<style scoped> |
| 211 | +.container, |
| 212 | +.wrapper, |
| 213 | +.outline, |
| 214 | +.handle { |
| 215 | + position: absolute; |
| 216 | + top: 50%; |
| 217 | + left: 50%; |
| 218 | +} |
| 219 | +
|
| 220 | +.container.hide { |
| 221 | + /* opacity: 0; |
| 222 | + pointer-events: none; */ |
| 223 | +} |
| 224 | +
|
| 225 | +.dot-center { |
| 226 | + position: absolute; |
| 227 | + top: 50%; |
| 228 | + left: 50%; |
| 229 | + width: 10px; |
| 230 | + height: 10px; |
| 231 | + background-color: white; |
| 232 | + border-radius: 50%; |
| 233 | + transform: translate(-50%, -50%); |
| 234 | +} |
| 235 | +
|
| 236 | +.wrapper { |
| 237 | + width: 450px; |
| 238 | + height: 450px; |
| 239 | + border-radius: 50%; |
| 240 | + /* background-color: rgba(255, 0, 0, 0.513); */ |
| 241 | +} |
| 242 | +
|
| 243 | +.handle { |
| 244 | + width: 50px; |
| 245 | + height: 12px; |
| 246 | + border-radius: 5px; |
| 247 | + background-color: white; |
| 248 | + transform: translate(-50%, -50%); |
| 249 | + box-shadow: 0 0 3px #00000080; |
| 250 | +} |
| 251 | +
|
| 252 | +.outline { |
| 253 | + width: 100%; |
| 254 | + height: 100%; |
| 255 | + border-radius: 50%; |
| 256 | + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
| 257 | + outline: 5px dashed white; |
| 258 | +} |
| 259 | +</style> |
0 commit comments