├── .editorconfig ├── .gitignore ├── README.md ├── babel.config.js ├── dev.md ├── examples ├── App.vue ├── assets │ ├── bg.jpg │ ├── btn.png │ └── logo.png ├── components │ ├── common │ │ └── element.vue │ └── stage.vue ├── favicon.ico ├── index.html ├── lib │ └── utils │ │ └── index.js └── main.js ├── package.json ├── src ├── index.js └── vue-drag-rotate-resize │ ├── index.vue │ ├── rect-box.vue │ └── utils.js ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /lib 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## vue-drag-rotate-resize 2 | 3 | 4 | ### Demo 5 | [demo](https://chenxingyuoo.github.io/vue-drag-rotate-resize) 6 | 7 | ## 安装 8 | ``` 9 | npm install @chenxingyu_o/vue-drag-rotate-resize 10 | ``` 11 | 12 | ## 注册组件 13 | ```javescript 14 | import VueDragRotateResize from '@chenxingyu_o/vue-drag-rotate-resize' 15 | import '@chenxingyu_o/vue-drag-rotate-resize/lib/vue-drag-rotate-resize.css' 16 | Vue.use(VueDragRotateResize) 17 | ``` 18 | 19 | ## 使用组件 20 | ```vue 21 | 49 | 50 | 86 | ``` 87 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | # vue-drag-rotate-resize 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn run lint 21 | ``` 22 | 23 | ### Run your unit tests 24 | ``` 25 | yarn run test:unit 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /examples/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxingyuoo/vue-drag-rotate-resize/23493d93878d2d9947fb0ae0a7ff978f4192ec74/examples/assets/bg.jpg -------------------------------------------------------------------------------- /examples/assets/btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxingyuoo/vue-drag-rotate-resize/23493d93878d2d9947fb0ae0a7ff978f4192ec74/examples/assets/btn.png -------------------------------------------------------------------------------- /examples/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxingyuoo/vue-drag-rotate-resize/23493d93878d2d9947fb0ae0a7ff978f4192ec74/examples/assets/logo.png -------------------------------------------------------------------------------- /examples/components/common/element.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | 57 | 66 | -------------------------------------------------------------------------------- /examples/components/stage.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 236 | 237 | 256 | -------------------------------------------------------------------------------- /examples/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxingyuoo/vue-drag-rotate-resize/23493d93878d2d9947fb0ae0a7ff978f4192ec74/examples/favicon.ico -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-drag-rotate-resize 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/lib/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取2个平面的宽比、高比 3 | * @param {object} originRect 原宽高对象 4 | * @param {object} targetRect 目标宽高对象 5 | * @return {object} 宽高比 6 | * @example 7 | * getRect2WHRatio({width: 100, height: 100}, {width: 200, height: 200}) 8 | * // => {heightRatio: 2,maxRatio: 2, minRatio: 2, widthRatio: 2} 9 | */ 10 | export const getRect2WHRatio = (originRect, targetRect) => { 11 | const widthRatio = targetRect.width / originRect.width 12 | const heightRatio = targetRect.height / originRect.height 13 | return { 14 | widthRatio: widthRatio, 15 | heightRatio: heightRatio, 16 | maxRatio: Math.max(widthRatio, heightRatio), 17 | minRatio: Math.min(widthRatio, heightRatio) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import VueDragRotateResize from '../src/index' 4 | Vue.use(VueDragRotateResize) 5 | 6 | Vue.config.productionTip = false 7 | 8 | new Vue({ 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chenxingyu_o/vue-drag-rotate-resize", 3 | "version": "1.0.0", 4 | "author": "chenxignyuoo", 5 | "description": "a vue2 component. drag-rotate-resize", 6 | "main": "lib/vue-drag-rotate-resize.common.js", 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build ./src/index.js --dest lib --target lib --name vue-drag-rotate-resize", 10 | "build-demo": "vue-cli-service build", 11 | "lint": "vue-cli-service lint", 12 | "test:unit": "vue-cli-service test:unit" 13 | }, 14 | "files": [ 15 | "lib", 16 | "src" 17 | ], 18 | "keywords": [ 19 | "vue", 20 | "drag", 21 | "resize", 22 | "rotate", 23 | "draggable", 24 | "resizable", 25 | "rotateable" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/chenxingyuoo/vue-drag-rotate-resize.git" 30 | }, 31 | "dependencies": { 32 | "core-js": "^2.6.5", 33 | "vue": "^2.6.10" 34 | }, 35 | "devDependencies": { 36 | "@vue/cli-plugin-babel": "^3.0.1", 37 | "@vue/cli-plugin-eslint": "^3.0.1", 38 | "@vue/cli-plugin-unit-jest": "^3.0.1", 39 | "@vue/cli-service": "^3.0.1", 40 | "@vue/eslint-config-standard": "^4.0.0", 41 | "@vue/test-utils": "1.0.0-beta.29", 42 | "babel-core": "7.0.0-bridge.0", 43 | "babel-eslint": "^10.0.1", 44 | "babel-jest": "^23.6.0", 45 | "eslint": "^5.16.0", 46 | "eslint-plugin-vue": "^5.0.0", 47 | "less": "^3.0.4", 48 | "less-loader": "^4.1.0", 49 | "vue-template-compiler": "^2.6.10" 50 | }, 51 | "eslintConfig": { 52 | "root": true, 53 | "env": { 54 | "node": true 55 | }, 56 | "extends": [ 57 | "plugin:vue/essential", 58 | "@vue/standard" 59 | ], 60 | "rules": {}, 61 | "parserOptions": { 62 | "parser": "babel-eslint" 63 | } 64 | }, 65 | "postcss": { 66 | "plugins": { 67 | "autoprefixer": {} 68 | } 69 | }, 70 | "browserslist": [ 71 | "> 1%", 72 | "last 2 versions" 73 | ], 74 | "jest": { 75 | "moduleFileExtensions": [ 76 | "js", 77 | "jsx", 78 | "json", 79 | "vue" 80 | ], 81 | "transform": { 82 | "^.+\\.vue$": "vue-jest", 83 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", 84 | "^.+\\.jsx?$": "babel-jest" 85 | }, 86 | "transformIgnorePatterns": [ 87 | "/node_modules/" 88 | ], 89 | "moduleNameMapper": { 90 | "^@/(.*)$": "/src/$1" 91 | }, 92 | "snapshotSerializers": [ 93 | "jest-serializer-vue" 94 | ], 95 | "testMatch": [ 96 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 97 | ], 98 | "testURL": "http://localhost/", 99 | "watchPlugins": [ 100 | "jest-watch-typeahead/filename", 101 | "jest-watch-typeahead/testname" 102 | ] 103 | }, 104 | "license": "MIT" 105 | } 106 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DragRotateResize from './vue-drag-rotate-resize/index.vue' 2 | 3 | DragRotateResize.install = function (Vue) { 4 | Vue.component(DragRotateResize.name, DragRotateResize) 5 | } 6 | 7 | /* istanbul ignore if */ 8 | if (typeof window !== 'undefined' && window.Vue) { 9 | DragRotateResize.install(window.Vue) 10 | } 11 | 12 | export default DragRotateResize 13 | -------------------------------------------------------------------------------- /src/vue-drag-rotate-resize/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 212 | -------------------------------------------------------------------------------- /src/vue-drag-rotate-resize/rect-box.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 254 | 255 | 356 | -------------------------------------------------------------------------------- /src/vue-drag-rotate-resize/utils.js: -------------------------------------------------------------------------------- 1 | export const getLength = (x, y) => Math.sqrt(x * x + y * y) 2 | 3 | export const getAngle = ({ x: x1, y: y1 }, { x: x2, y: y2 }) => { 4 | const dot = x1 * x2 + y1 * y2 5 | const det = x1 * y2 - y1 * x2 6 | const angle = Math.atan2(det, dot) / Math.PI * 180 7 | return (angle + 360) % 360 8 | } 9 | 10 | export const degToRadian = (deg) => deg * Math.PI / 180 11 | 12 | const cos = (deg) => Math.cos(degToRadian(deg)) 13 | const sin = (deg) => Math.sin(degToRadian(deg)) 14 | 15 | const setWidthAndDeltaW = (width, deltaW, minWidth) => { 16 | const expectedWidth = width + deltaW 17 | if (expectedWidth > minWidth) { 18 | width = expectedWidth 19 | } else { 20 | deltaW = minWidth - width 21 | width = minWidth 22 | } 23 | return { width, deltaW } 24 | } 25 | 26 | const setHeightAndDeltaH = (height, deltaH, minHeight) => { 27 | const expectedHeight = height + deltaH 28 | if (expectedHeight > minHeight) { 29 | height = expectedHeight 30 | } else { 31 | deltaH = minHeight - height 32 | height = minHeight 33 | } 34 | return { height, deltaH } 35 | } 36 | 37 | export const getNewStyle = (type, rect, deltaW, deltaH, ratio, minWidth, minHeight) => { 38 | let { width, height, centerX, centerY, rotateAngle } = rect 39 | const widthFlag = width < 0 ? -1 : 1 40 | const heightFlag = height < 0 ? -1 : 1 41 | width = Math.abs(width) 42 | height = Math.abs(height) 43 | switch (type) { 44 | case 'r': { 45 | const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth) 46 | width = widthAndDeltaW.width 47 | deltaW = widthAndDeltaW.deltaW 48 | if (ratio) { 49 | deltaH = deltaW / ratio 50 | height = width / ratio 51 | // 左上角固定 52 | centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle) 53 | centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle) 54 | } else { 55 | // 左边固定 56 | centerX += deltaW / 2 * cos(rotateAngle) 57 | centerY += deltaW / 2 * sin(rotateAngle) 58 | } 59 | break 60 | } 61 | case 'tr': { 62 | deltaH = -deltaH 63 | const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth) 64 | width = widthAndDeltaW.width 65 | deltaW = widthAndDeltaW.deltaW 66 | const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight) 67 | height = heightAndDeltaH.height 68 | deltaH = heightAndDeltaH.deltaH 69 | if (ratio) { 70 | deltaW = deltaH * ratio 71 | width = height * ratio 72 | } 73 | centerX += deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle) 74 | centerY += deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle) 75 | break 76 | } 77 | case 'br': { 78 | const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth) 79 | width = widthAndDeltaW.width 80 | deltaW = widthAndDeltaW.deltaW 81 | const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight) 82 | height = heightAndDeltaH.height 83 | deltaH = heightAndDeltaH.deltaH 84 | if (ratio) { 85 | deltaW = deltaH * ratio 86 | width = height * ratio 87 | } 88 | centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle) 89 | centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle) 90 | break 91 | } 92 | case 'b': { 93 | const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight) 94 | height = heightAndDeltaH.height 95 | deltaH = heightAndDeltaH.deltaH 96 | if (ratio) { 97 | deltaW = deltaH * ratio 98 | width = height * ratio 99 | // 左上角固定 100 | centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle) 101 | centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle) 102 | } else { 103 | // 上边固定 104 | centerX -= deltaH / 2 * sin(rotateAngle) 105 | centerY += deltaH / 2 * cos(rotateAngle) 106 | } 107 | break 108 | } 109 | case 'bl': { 110 | deltaW = -deltaW 111 | const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth) 112 | width = widthAndDeltaW.width 113 | deltaW = widthAndDeltaW.deltaW 114 | const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight) 115 | height = heightAndDeltaH.height 116 | deltaH = heightAndDeltaH.deltaH 117 | if (ratio) { 118 | height = width / ratio 119 | deltaH = deltaW / ratio 120 | } 121 | centerX -= deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle) 122 | centerY -= deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle) 123 | break 124 | } 125 | case 'l': { 126 | deltaW = -deltaW 127 | const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth) 128 | width = widthAndDeltaW.width 129 | deltaW = widthAndDeltaW.deltaW 130 | if (ratio) { 131 | height = width / ratio 132 | deltaH = deltaW / ratio 133 | // 右上角固定 134 | centerX -= deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle) 135 | centerY -= deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle) 136 | } else { 137 | // 右边固定 138 | centerX -= deltaW / 2 * cos(rotateAngle) 139 | centerY -= deltaW / 2 * sin(rotateAngle) 140 | } 141 | break 142 | } 143 | case 'tl': { 144 | deltaW = -deltaW 145 | deltaH = -deltaH 146 | const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth) 147 | width = widthAndDeltaW.width 148 | deltaW = widthAndDeltaW.deltaW 149 | const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight) 150 | height = heightAndDeltaH.height 151 | deltaH = heightAndDeltaH.deltaH 152 | if (ratio) { 153 | width = height * ratio 154 | deltaW = deltaH * ratio 155 | } 156 | centerX -= deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle) 157 | centerY -= deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle) 158 | break 159 | } 160 | case 't': { 161 | deltaH = -deltaH 162 | const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight) 163 | height = heightAndDeltaH.height 164 | deltaH = heightAndDeltaH.deltaH 165 | if (ratio) { 166 | width = height * ratio 167 | deltaW = deltaH * ratio 168 | // 左下角固定 169 | centerX += deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle) 170 | centerY += deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle) 171 | } else { 172 | centerX += deltaH / 2 * sin(rotateAngle) 173 | centerY -= deltaH / 2 * cos(rotateAngle) 174 | } 175 | break 176 | } 177 | } 178 | 179 | return { 180 | position: { 181 | centerX, 182 | centerY 183 | }, 184 | size: { 185 | width: width * widthFlag, 186 | height: height * heightFlag 187 | } 188 | } 189 | } 190 | 191 | const cursorStartMap = { n: 0, ne: 1, e: 2, se: 3, s: 4, sw: 5, w: 6, nw: 7 } 192 | // const cursorDirectionArray = [ 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw' ] 193 | const cursorDirectionArray = ['ns', 'nesw', 'ew', 'nwse', 'ns', 'nesw', 'ew', 'nwse'] 194 | const cursorMap = { 0: 0, 1: 1, 2: 2, 3: 2, 4: 3, 5: 4, 6: 4, 7: 5, 8: 6, 9: 6, 10: 7, 11: 8 } 195 | export const getCursor = (rotateAngle, d) => { 196 | const increment = cursorMap[ Math.floor(rotateAngle / 30) ] 197 | const index = cursorStartMap[ d ] 198 | const newIndex = (index + increment) % 8 199 | return cursorDirectionArray[ newIndex ] 200 | } 201 | 202 | export const centerToTL = ({ centerX, centerY, width, height, rotateAngle }) => ({ 203 | top: centerY - height / 2, 204 | left: centerX - width / 2, 205 | width, 206 | height, 207 | rotateAngle 208 | }) 209 | 210 | export const tLToCenter = ({ top, left, width, height, rotateAngle }) => ({ 211 | position: { 212 | centerX: left + width / 2, 213 | centerY: top + height / 2 214 | }, 215 | size: { 216 | width, 217 | height 218 | }, 219 | transform: { 220 | rotateAngle 221 | } 222 | }) 223 | 224 | /** 225 | * 获取一个矩形的中心点 226 | * @param {object} rect 矩形宽高对象 227 | * @return {object} 中心点坐标 228 | * @example 229 | * getRectCenter({width: 100, height: 100}) 230 | * // => {centerX: 50, centerY: 50} 231 | */ 232 | export const getRectCenter = (rect) => { 233 | return { 234 | centerX: rect.width / 2, 235 | centerY: rect.height / 2 236 | } 237 | } 238 | 239 | /** 240 | * 获取2个矩形的中心点偏移值 241 | * @param {object} originRect 原宽高对象 242 | * @param {object} targetRect 目标宽高对象 243 | * @example 244 | * getRect2CenterTranslate({width: 100, height: 100}, {width: 200, height: 200}) 245 | * // => {left: 50, top: 50} 246 | */ 247 | export const getRect2CenterTranslate = (originRect, targetRect) => { 248 | const targetCenter = getRectCenter(targetRect) 249 | const originCenter = getRectCenter(originRect) 250 | return { 251 | top: targetCenter.centerY - originCenter.centerY, 252 | left: targetCenter.centerX - originCenter.centerX 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message' 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }) 10 | expect(wrapper.text()).toMatch(msg) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pages: { 3 | index: { 4 | entry: 'examples/main.js', 5 | template: 'examples/index.html', 6 | filename: 'index.html' 7 | } 8 | } 9 | } 10 | --------------------------------------------------------------------------------