├── babel.config.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── views │ ├── About.vue │ └── Home.vue ├── store │ ├── index.js │ └── modules │ │ ├── template.js │ │ └── material.js ├── main.js ├── router.js ├── App.vue ├── libs │ ├── util.js │ ├── config.js │ └── list.js └── components │ ├── MaterialStage │ ├── LayerFont.vue │ ├── index.vue │ └── LayerGroup.vue │ ├── MaterialConfig.vue │ ├── TemplateStage │ ├── LayerFont.vue │ ├── index.vue │ └── LayerGroup.vue │ ├── CommonSliderBar.vue │ ├── Material.vue │ ├── Template.vue │ └── TemplateConfig.vue ├── .gitignore ├── README.md └── package.json /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrLambs/vue-konva-project/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrLambs/vue-konva-project/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | pagckage-lock.json 24 | yarn.lock 25 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import template from './modules/template' 4 | import material from './modules/material' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | state: { 10 | // 11 | }, 12 | mutations: { 13 | // 14 | }, 15 | actions: { 16 | // 17 | }, 18 | modules: { 19 | template, 20 | material 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import iView from 'iview' 6 | import 'iview/dist/styles/iview.css' 7 | import VueKonva from 'vue-konva' 8 | 9 | Vue.use(iView) 10 | Vue.use(VueKonva) 11 | 12 | Vue.config.productionTip = false 13 | 14 | new Vue({ 15 | router, 16 | store, 17 | render: h => h(App) 18 | }).$mount('#app') 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-konva-project 2 | 3 | ## [Vue Konva - 基于 Canvas 开发的 2d JavaScript框架库 - 使用整理【持续更新……】](https://blog.csdn.net/sansan_7957/article/details/81952061) 4 | 5 | ## 注意:需要用一下命令打开谷歌以解决跨域问题 6 | ``` 7 | open -a /Applications/Google\ Chrome.app --args --disable-web-security --user-data-dir 8 | ``` 9 | 10 | ## Project setup 11 | ``` 12 | yarn install 13 | ``` 14 | 15 | ### Compiles and hot-reloads for development 16 | ``` 17 | yarn run serve 18 | ``` 19 | 20 | ### Compiles and minifies for production 21 | ``` 22 | yarn run build 23 | ``` 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-konva-project 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Template from './components/Template' 4 | import Material from './components/Material' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | mode: 'history', 10 | base: process.env.BASE_URL, 11 | routes: [ 12 | // { 13 | // path: '/', 14 | // name: 'home', 15 | // component: Home 16 | // }, 17 | { 18 | path: '/', 19 | name: 'template', 20 | component: Template 21 | }, 22 | { 23 | path: '/material', 24 | name: 'material', 25 | // route level code-splitting 26 | // this generates a separate chunk (about.[hash].js) for this route 27 | // which is lazy-loaded when the route is visited. 28 | component: Material 29 | } 30 | ] 31 | }) 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /src/libs/util.js: -------------------------------------------------------------------------------- 1 | import { Message } from "iview"; 2 | const util = {}; 3 | 4 | /** 5 | * 错误提示 6 | * @param msg 7 | */ 8 | util.showError = function (msg) { 9 | Message.destroy() 10 | Message.error({ 11 | content: msg, 12 | duration: 30, 13 | closable: true 14 | }) 15 | } 16 | /** 17 | * 成功提示 18 | * @param msg 19 | */ 20 | util.showSuccess = function (msg) { 21 | Message.destroy() 22 | Message.success({ 23 | content: msg, 24 | duration: 5, 25 | closable: true 26 | }) 27 | } 28 | /** 29 | * 警告提示 30 | * @param msg 31 | */ 32 | util.showWarning = function (msg) { 33 | Message.destroy() 34 | Message.warning({ 35 | content: msg, 36 | duration: 10, 37 | closable: true 38 | }) 39 | } 40 | /** 41 | * 生成图片 42 | * @param src 图片路径 43 | */ 44 | util.newImage = function (src) { 45 | let img = new Image(); 46 | img.src = src; 47 | return img; 48 | } 49 | 50 | export default util; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-konva-project", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "iview": "^3.3.3", 12 | "konva": "^3.2.2", 13 | "lodash": "^4.17.11", 14 | "vue": "^2.6.6", 15 | "vue-konva": "^2.0.3", 16 | "vue-router": "^3.0.1", 17 | "vuex": "^3.0.1" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^3.0.5", 21 | "@vue/cli-plugin-eslint": "^3.0.5", 22 | "@vue/cli-service": "^3.0.5", 23 | "babel-eslint": "^10.0.1", 24 | "eslint": "^5.8.0", 25 | "eslint-plugin-vue": "^5.0.0", 26 | "less": "^3.9.0", 27 | "less-loader": "^4.1.0", 28 | "vue-template-compiler": "^2.5.21" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "eslint:recommended" 38 | ], 39 | "rules": {}, 40 | "parserOptions": { 41 | "parser": "babel-eslint" 42 | } 43 | }, 44 | "postcss": { 45 | "plugins": { 46 | "autoprefixer": {} 47 | } 48 | }, 49 | "browserslist": [ 50 | "> 1%", 51 | "last 2 versions", 52 | "not ie <= 8" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/components/MaterialStage/LayerFont.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 62 | -------------------------------------------------------------------------------- /src/libs/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | group: { 4 | uniqueId: 0, 5 | type: 'rect', 6 | x: 10, 7 | y: 10, 8 | width: 188, 9 | height: 244, 10 | draggable: true, 11 | rotation: 0, 12 | scale: 1, 13 | disabled: false, 14 | side: 300, 15 | radius: 150, 16 | stroke: 'transparent', 17 | strokeWidth: 1, 18 | src: 'http://uploads.5068.com/allimg/1712/151-1G204111207.jpg' 19 | }, 20 | font: { 21 | uniqueId: 0, 22 | type: 'font', 23 | x: 200, 24 | y: 200, 25 | width: 246, 26 | height: 149, 27 | draggable: true, 28 | rotation: 0, 29 | scale: 1, 30 | fontText: '在这里输入文字', 31 | fontFamily: 'Sans-serif', 32 | fontSize: 24, 33 | fontColor: '#70FFF5', 34 | disabled: false, 35 | stroke: 'transparent', 36 | strokeWidth: 1, 37 | src: 'http://uploads.5068.com/allimg/1712/151-1G2050U519.jpg' 38 | } 39 | }, 40 | initConfig: { 41 | group: { 42 | uniqueId: 0, 43 | type: 'rect', 44 | x: 10, 45 | y: 1, 46 | width: 188, 47 | height: 244, 48 | draggable: true, 49 | rotation: 0, 50 | scale: 1, 51 | disabled: false, 52 | side: 300, 53 | radius: 150, 54 | stroke: 'transparent', 55 | strokeWidth: 1, 56 | src: 'http://uploads.5068.com/allimg/1712/151-1G204111207.jpg' 57 | }, 58 | font: { 59 | uniqueId: 0, 60 | type: 'font', 61 | x: 200, 62 | y: 200, 63 | width: 246, 64 | height: 149, 65 | draggable: true, 66 | rotation: 0, 67 | scale: 1, 68 | fontText: '在这里输入文字', 69 | fontFamily: 'Sans-serif', 70 | fontSize: 24, 71 | fontColor: '#70FFF5', 72 | disabled: false, 73 | stroke: 'transparent', 74 | strokeWidth: 1, 75 | src: 'http://uploads.5068.com/allimg/1712/151-1G2050U519.jpg' 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/components/MaterialConfig.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 63 | 64 | 75 | -------------------------------------------------------------------------------- /src/libs/list.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 形状列表 3 | shapeList: [ 4 | { 5 | label: "矩形", 6 | value: "rect" 7 | } 8 | // { 9 | // label: '圆形', 10 | // value: 'circle' 11 | // }, 12 | // { 13 | // label: '三角形', 14 | // value: 'triangle' 15 | // }, 16 | // { 17 | // label: '心形', 18 | // value: 'heart' 19 | // } 20 | ], 21 | // 装饰类型 22 | decorationList: [ 23 | { 24 | label: "文字", 25 | value: "font" 26 | }, 27 | { 28 | label: "图片", 29 | value: "image" 30 | } 31 | ], 32 | // 字体列表 33 | fontFamilyList: [ 34 | { 35 | id: 0, 36 | value: "Sans-serif" 37 | }, 38 | { 39 | id: 1, 40 | value: "Helvetica" 41 | }, 42 | { 43 | id: 2, 44 | value: "Arial" 45 | }, 46 | { 47 | id: 3, 48 | value: "Verdana" 49 | }, 50 | { 51 | id: 4, 52 | value: "Tahoma" 53 | }, 54 | { 55 | id: 5, 56 | value: "Lucida Grande" 57 | }, 58 | { 59 | id: 6, 60 | value: "Georgia" 61 | }, 62 | { 63 | id: 7, 64 | value: "Arial Black" 65 | } 66 | ], 67 | // 字号列表 68 | fontSizeList: [ 69 | { 70 | id: 12, 71 | value: "12px" 72 | }, 73 | { 74 | id: 14, 75 | value: "14px" 76 | }, 77 | { 78 | id: 16, 79 | value: "16px" 80 | }, 81 | { 82 | id: 18, 83 | value: "18px" 84 | }, 85 | { 86 | id: 20, 87 | value: "20px" 88 | }, 89 | { 90 | id: 24, 91 | value: "24px" 92 | }, 93 | { 94 | id: 32, 95 | value: "32px" 96 | }, 97 | { 98 | id: 36, 99 | value: "36px" 100 | }, 101 | { 102 | id: 44, 103 | value: "44px" 104 | }, 105 | { 106 | id: 56, 107 | value: "56px" 108 | }, 109 | { 110 | id: 60, 111 | value: "60px" 112 | }, 113 | { 114 | id: 72, 115 | value: "72px" 116 | } 117 | ] 118 | } -------------------------------------------------------------------------------- /src/components/MaterialStage/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 96 | -------------------------------------------------------------------------------- /src/components/TemplateStage/LayerFont.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 100 | -------------------------------------------------------------------------------- /src/store/modules/template.js: -------------------------------------------------------------------------------- 1 | import config from "../../libs/config"; 2 | 3 | const template = { 4 | state: { 5 | stageConfig: { 6 | width: 960, 7 | height: 600, 8 | url: "http://img.pconline.com.cn/images/upload/upc/tx/wallpaper/1212/27/c0/16922564_1356570216970.jpg" 9 | }, 10 | // config 类型: 模板框/装饰 11 | configType: 'group', 12 | // 表单初始化 13 | initConfig: config.initConfig, 14 | // 表单 15 | config: config.config, 16 | // 配置列表 17 | configList: { 18 | group: [], 19 | font: [] 20 | }, 21 | // 记录下一开始添加时的唯一ID 22 | uniqueId: { 23 | group: 0, 24 | font: 0 25 | }, 26 | // 当前选中的ID 27 | currentId: { 28 | group: 0, 29 | font: 0 30 | }, 31 | // 拖动定位 32 | dragStartX: 0, 33 | dragStartY: 0 34 | }, 35 | mutations: { 36 | // 设置当前配置类型: group/font 37 | setConfigType(state, type) { 38 | state.configType = type; 39 | }, 40 | // 添加配置 41 | add(state, config) { 42 | // id 增加 43 | state.config[state.configType].uniqueId++; 44 | state.uniqueId[state.configType] = state.config[state.configType].uniqueId; 45 | // 添加 46 | state.configList[state.configType].push(config); 47 | }, 48 | // 保存配置 49 | save(state, config) { 50 | state.configList[state.configType].forEach((item, index) => { 51 | if (parseInt(item['uniqueId']) === state.currentId[state.configType]) { 52 | // 替换原来的 53 | state.configList[state.configType].splice(index, 1, config); 54 | } 55 | }); 56 | // 初始化 57 | state.initConfig[state.configType].uniqueId = state.uniqueId[state.configType]; 58 | state.config[state.configType] = state.initConfig[state.configType]; 59 | }, 60 | // 删除配置 61 | remove(state) { 62 | state.configList[state.configType].forEach((item, index) => { 63 | if (parseInt(item['uniqueId']) === state.currentId[state.configType]) { 64 | // 删除 65 | state.configList[state.configType].splice(index, 1); 66 | } 67 | }); 68 | // 初始化 69 | state.initConfig[state.configType].uniqueId = state.uniqueId[state.configType]; 70 | state.config[state.configType] = state.initConfig[state.configType]; 71 | }, 72 | // 获取当前配置 73 | current(state, config) { 74 | state.configList[state.configType].forEach(item => { 75 | item.stroke = 'transparent'; 76 | }); 77 | state.config[state.configType] = config; 78 | state.config[state.configType].stroke = '#0051FF'; 79 | state.currentId[state.configType] = config.uniqueId; 80 | }, 81 | start(state, {x, y}) { 82 | state.dragStartX = x - state.config[state.configType].x; 83 | state.dragStartY = y - state.config[state.configType].y; 84 | }, 85 | move(state, {x, y}) { 86 | state.config[state.configType].x = x - state.dragStartX; 87 | state.config[state.configType].y = y - state.dragStartY; 88 | }, 89 | end(state, {x, y}) { 90 | state.config[state.configType].x = x - state.dragStartX; 91 | state.config[state.configType].y = y - state.dragStartY; 92 | } 93 | } 94 | } 95 | 96 | export default template; 97 | -------------------------------------------------------------------------------- /src/components/TemplateStage/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 104 | 105 | 115 | -------------------------------------------------------------------------------- /src/store/modules/material.js: -------------------------------------------------------------------------------- 1 | import util from '../../libs/util'; 2 | 3 | const material = { 4 | state: { 5 | // 当前选中的模版 6 | currentTemplate: null, 7 | // 当前选中的 group 8 | curId: -1, 9 | // 当前选中的 v-image 10 | config: { 11 | x: 0, 12 | y: 0, 13 | rotation: 0, 14 | scale: 0, 15 | src: '' 16 | }, 17 | // 拖动定位 18 | dragStartX: 0, 19 | dragStartY: 0 20 | }, 21 | mutations: { 22 | // 获取当前使用的模版配置 23 | getCurrentTepmlate(state, config) { 24 | state.currentTemplate = config; 25 | }, 26 | // 选中的 v-image 配置与表单同步 27 | setImageId(state, id) { 28 | state.curId = id 29 | state.currentTemplate.configList.group.forEach(item => { 30 | if (item.uniqueId === id) { 31 | state.config = item.vImage 32 | } 33 | }) 34 | }, 35 | // 创建素材-更换图片 36 | setUrl(state, img) { 37 | const url = img.src; 38 | const width = img.width; 39 | const height = img.height; 40 | const originWidth = width; 41 | const originHeight = height; 42 | const face_x = face_x ? face_x : width / 2; 43 | const face_y = face_y ? face_y : height / 2; 44 | 45 | // 替换模板中的group图片 46 | state.currentTemplate.configList.group.forEach(item => { 47 | if (item.uniqueId === state.curId) { 48 | item.vImage.src = url; 49 | 50 | // 设置图片大小填充group框 51 | let _img = util.newImage(url); 52 | _img.onload = function () { 53 | // 图片宽高比 54 | const perImg = parseFloat((_img.width / _img.height).toFixed(2)); 55 | // group 框宽高比 56 | const perGroup = parseFloat((item.width / item.height).toFixed(2)); 57 | 58 | if (perImg >= perGroup) { 59 | item.vImage.height = item.height; 60 | item.vImage.width = Math.ceil(item.height * perImg); 61 | 62 | // 图片缩放比例 x 63 | const perX = parseFloat((item.vImage.width / originWidth).toFixed(2)); 64 | // group 框中心点 x 65 | const middleX = parseFloat((item.width / 2).toFixed(2)); 66 | // 图片缩放之后的人脸坐标 67 | const faceX = parseFloat((face_x * perX).toFixed(2)); 68 | // 人脸居中:修改图片坐标 face_x 69 | item.vImage.x = Math.floor(middleX - faceX); 70 | if (item.vImage.x > 0) { 71 | item.vImage.x = 0; 72 | } else if (item.vImage.x < item.width - item.vImage.width) { 73 | item.vImage.x = item.width - item.vImage.width; 74 | } 75 | } else if (perImg < perGroup) { 76 | item.vImage.width = item.width; 77 | item.vImage.height = Math.ceil(item.width / perImg); 78 | 79 | // 图片缩放比例 y 80 | const perY = parseFloat((item.vImage.height / originHeight).toFixed(2)); 81 | // group 框中心点 x 82 | const middleY = parseFloat((item.height / 2).toFixed(2)); 83 | // 图片缩放之后的人脸坐标 84 | const faceY = parseFloat((face_y * perY).toFixed(2)); 85 | // 人脸居中:修改图片坐标 face_y 86 | item.vImage.y = Math.floor(middleY - faceY); 87 | if (item.vImage.y > 0) { 88 | item.vImage.y = 0; 89 | } else if (item.vImage.y < item.height - item.vImage.height) { 90 | item.vImage.y = item.height - item.vImage.height; 91 | } 92 | } 93 | item.vImage.img = _img; 94 | } 95 | } 96 | }); 97 | }, 98 | start(state, { 99 | x, 100 | y 101 | }) { 102 | state.dragStartX = x - state.config.x; 103 | state.dragStartY = y - state.config.y; 104 | }, 105 | move(state, { 106 | x, 107 | y 108 | }) { 109 | state.config.x = x - state.dragStartX; 110 | state.config.y = y - state.dragStartY; 111 | }, 112 | end(state, { 113 | x, 114 | y 115 | }) { 116 | state.config.x = x - state.dragStartX; 117 | state.config.y = y - state.dragStartY; 118 | } 119 | } 120 | } 121 | 122 | export default material -------------------------------------------------------------------------------- /src/components/TemplateStage/LayerGroup.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 136 | -------------------------------------------------------------------------------- /src/components/MaterialStage/LayerGroup.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 132 | -------------------------------------------------------------------------------- /src/components/CommonSliderBar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 106 | 107 | 195 | -------------------------------------------------------------------------------- /src/components/Material.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 129 | 130 | 198 | -------------------------------------------------------------------------------- /src/components/Template.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 158 | 159 | 186 | -------------------------------------------------------------------------------- /src/components/TemplateConfig.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 191 | --------------------------------------------------------------------------------