mirror of
https://github.com/palxiao/poster-design.git
synced 2025-07-15 16:02:19 +08:00
docs: readme
This commit is contained in:
parent
2c37bf84ee
commit
9b9796e571
38
README.md
38
README.md
@ -1,10 +1,12 @@
|
|||||||
[在线体验](https://design.palxp.com/) | [文档网站](https://xp.palxp.com/) | [项目架构及目录讲解](https://xp.palxp.com/#/articles/1689321259854)
|
[在线体验](https://design.palxp.com/) | [文档网站](https://xp.palxp.com/) | [项目架构及目录](https://xp.palxp.com/#/articles/1689321259854)
|
||||||
|
|
||||||
## 迅排设计
|
## 迅排设计
|
||||||
|
|
||||||
一款漂亮且功能强大的在线海报图片设计器,仿稿定设计。适用于海报图片生成、电商分享图、文章长图、视频/公众号封面等多种场景。
|
一款漂亮且功能强大的在线海报图片设计器,仿稿定设计。
|
||||||
|
|
||||||

|
适用于海报图片生成、电商分享图、文章长图、视频/公众号封面等多种场景。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
- 丝滑的操作体验,丰富的交互细节,基础功能完善
|
- 丝滑的操作体验,丰富的交互细节,基础功能完善
|
||||||
- 采用服务端生成图片,确保多端出图统一性,支持各种 CSS 特性
|
- 采用服务端生成图片,确保多端出图统一性,支持各种 CSS 特性
|
||||||
@ -12,14 +14,16 @@
|
|||||||
|
|
||||||
### 技术栈概括
|
### 技术栈概括
|
||||||
|
|
||||||
前端:Vue3 、Vite2 、Vuex 、ElementPlus
|
- Vue3 、Vite2 、Vuex 、ElementPlus
|
||||||
|
|
||||||
图片生成:Puppeteer、Express
|
- 图片生成:Puppeteer、Express
|
||||||
|
|
||||||
一些可独立的功能会被抽取出来作为单独的库引入使用,仓库地址:[front-end-arsenal](https://github.com/palxiao/front-end-arsenal),[组件文档网站](https://fe-doc.palxp.com/#/)
|
一些可独立的功能会被抽取出来作为单独的库引入使用,仓库地址:[front-end-arsenal](https://github.com/palxiao/front-end-arsenal)([组件文档网站](https://fe-doc.palxp.com/#/))
|
||||||
|
|
||||||
> 环境需求:**Node.js v16** 以上版本
|
> 环境需求:**Node.js v16** 以上版本
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
### 拉取源码
|
### 拉取源码
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -43,28 +47,22 @@ npm run serve
|
|||||||
>
|
>
|
||||||
> 
|
> 
|
||||||
|
|
||||||
### 运行结果
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
合成图片时本地会启动一个 Chrome 浏览器实例。
|
合成图片时本地会启动一个 Chrome 浏览器实例。
|
||||||
|
|
||||||
### 打包前端页面
|
### 打包
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run v-build
|
npm run v-build <- 前端页面
|
||||||
```
|
npm run build <- 图片生成服务( sreenshot 目录下)
|
||||||
|
|
||||||
### 打包图片生成服务
|
|
||||||
|
|
||||||
```
|
|
||||||
cd sreenshot
|
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 服务端
|
### 服务端
|
||||||
|
|
||||||
可参考接口 API 文档自行实现服务端。
|
可参考[接口 API 文档](https://xp.palxp.com/apidoc/index.html)自行实现服务端。
|
||||||
|
|
||||||
|
### 图片生成服务
|
||||||
|
|
||||||
|
代码位于 `screenshots` 目录下,[接口 API 文档](https://xp.palxp.com/apidoc/screenshot.html)。
|
||||||
|
|
||||||
### 服务器配置
|
### 服务器配置
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* @Date: 2022-02-01 13:41:59
|
* @Date: 2022-02-01 13:41:59
|
||||||
* @Description:
|
* @Description:
|
||||||
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
* @LastEditTime: 2023-07-04 19:04:44
|
* @LastEditTime: 2023-07-18 16:30:53
|
||||||
-->
|
-->
|
||||||
# Node截图服务
|
# Node截图服务
|
||||||
|
|
||||||
@ -15,6 +15,10 @@ ts-node 直接运行,并监听文件修改自动热重启
|
|||||||
|
|
||||||
使用 webpack 打包文件
|
使用 webpack 打包文件
|
||||||
|
|
||||||
|
### build:apidoc
|
||||||
|
|
||||||
|
生成 API 文档
|
||||||
|
|
||||||
### npm run serve (不使用)
|
### npm run serve (不使用)
|
||||||
|
|
||||||
webpack/tsc 编译 ts,supervisor/pm2 监听编译后文件变动重启服务,gulp 自动化任务
|
webpack/tsc 编译 ts,supervisor/pm2 监听编译后文件变动重启服务,gulp 自动化任务
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: ShawnPhang
|
|
||||||
* @Date: 2022-04-19 14:19:13
|
|
||||||
* @Description:
|
|
||||||
* @LastEditors: ShawnPhang
|
|
||||||
* @LastEditTime: 2022-04-21 18:38:10
|
|
||||||
*/
|
|
||||||
// const fs2 = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const puppeteer = require('puppeteer')
|
|
||||||
|
|
||||||
const GIFEncoder = require('gif-encoder-2')
|
|
||||||
const { createWriteStream } = require('fs')
|
|
||||||
const PNG = require('png-js')
|
|
||||||
|
|
||||||
const params = { width: 1242/3, height: 2208/3 }
|
|
||||||
|
|
||||||
function decode(png) {
|
|
||||||
return new Promise((r) => {
|
|
||||||
png.decode((pixels) => r(pixels))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gifAddFrame(page, encoder) {
|
|
||||||
const pngBuffer = await page.screenshot({ clip: { width: params.width, height: params.height, x: 0, y: 0 } })
|
|
||||||
const png = new PNG(pngBuffer)
|
|
||||||
await decode(png).then((pixels) => encoder.addFrame(pixels))
|
|
||||||
}
|
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
const browser = await puppeteer.launch({
|
|
||||||
headless: true,
|
|
||||||
slowMo: 0,
|
|
||||||
})
|
|
||||||
const page = await browser.newPage()
|
|
||||||
page.setViewport({ width: params.width, height: params.height })
|
|
||||||
await page.goto('http://localhost:3000/draw?tempid=519', {
|
|
||||||
waitUntil: ['networkidle0'],
|
|
||||||
timeout: 60000,
|
|
||||||
})
|
|
||||||
|
|
||||||
const dstPath = path.join(__dirname, `test.gif`)
|
|
||||||
// create a write stream for GIF data
|
|
||||||
const writeStream = createWriteStream(dstPath)
|
|
||||||
writeStream.on('close', () => {
|
|
||||||
console.log('create is done')
|
|
||||||
})
|
|
||||||
// createWriteStream().pipe(fs2.createWriteStream('test.gif'))
|
|
||||||
|
|
||||||
// setting gif encoder
|
|
||||||
// record gif
|
|
||||||
var encoder = new GIFEncoder(params.width, params.height)
|
|
||||||
encoder.createReadStream().pipe(writeStream)
|
|
||||||
encoder.start()
|
|
||||||
encoder.setRepeat(0)
|
|
||||||
encoder.setDelay(200)
|
|
||||||
// encoder.setQuality(10) // default
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await gifAddFrame(page, encoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// finish encoder, test.gif saved
|
|
||||||
encoder.finish()
|
|
||||||
|
|
||||||
await browser.close()
|
|
||||||
})()
|
|
@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: ShawnPhang
|
|
||||||
* @Date: 2022-02-28 15:35:59
|
|
||||||
* @Description:
|
|
||||||
* @LastEditors: ShawnPhang
|
|
||||||
* @LastEditTime: 2022-02-28 16:52:35
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = '/Users/mac/Documents/workSpace/Products/Management-Center/screenshot-service/static/screenshot-1-new.jpg'
|
|
||||||
|
|
||||||
const images = require('images')
|
|
||||||
|
|
||||||
const path233 = require('path')
|
|
||||||
// let tinyJpg = images(path233.resolve(__dirname, `../static/screenshot-1.png`)).size(300).encode('jpg', { quality: 20 })
|
|
||||||
// images(tinyJpg).save(path)
|
|
||||||
let tinyJpg = images(path233.resolve(__dirname, `../static/screenshot-1.png`)).size(300).save(path, { quality: 30 })
|
|
248
test/png.html
248
test/png.html
@ -1,248 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Document</title>
|
|
||||||
<style>
|
|
||||||
ul,
|
|
||||||
li {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 20px auto;
|
|
||||||
font-size: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 100px;
|
|
||||||
height: 50px;
|
|
||||||
padding: 0 20px;
|
|
||||||
margin: 1px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 50px;
|
|
||||||
color: #000;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s linear 0s;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="transform: scale(1);">
|
|
||||||
<input type="file" id="file">
|
|
||||||
<img style="width: 300px;height: 300px;" src="" id="img">
|
|
||||||
<p id="text"></p>
|
|
||||||
<ul id="ul"></ul>
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
// 封装函数库
|
|
||||||
function getImgColor(img) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ param 传入的图片
|
|
||||||
* @ this.progress 解析图片的进度 实时
|
|
||||||
* @ this.canvas canvas元素
|
|
||||||
* @ this.cvs context对象
|
|
||||||
* @ this.accuracy Number 解析图片颜色的精确度 1 - 7 数字选择
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @ anther taoqun <taoquns@foxmail.com>
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.canvas = document.createElement("canvas")
|
|
||||||
this.canvas.width = 300 || img.width
|
|
||||||
this.canvas.height = 300 || img.height
|
|
||||||
this.cvs = this.canvas.getContext("2d")
|
|
||||||
this.cvs.drawImage(img, 0, 0, 300, 300)
|
|
||||||
this.accuracy = 5
|
|
||||||
this.progress = ''
|
|
||||||
}
|
|
||||||
getImgColor.prototype.getColorXY = function (x, y) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param x Number x坐标起点
|
|
||||||
* @param y Number y坐标起点
|
|
||||||
* @return color Object 包含颜色的rgba #16进制颜色
|
|
||||||
*/
|
|
||||||
|
|
||||||
let obj = this.cvs.getImageData(x, y, 1, 1)
|
|
||||||
let arr = obj.data.toString().split(",")
|
|
||||||
|
|
||||||
let first = parseInt(arr[0]).toString(16)
|
|
||||||
first = first.length === 2 ? first : first + first
|
|
||||||
|
|
||||||
let second = parseInt(arr[1]).toString(16)
|
|
||||||
second = second.length === 2 ? second : second + second
|
|
||||||
|
|
||||||
let third = parseInt(arr[2]).toString(16)
|
|
||||||
third = third.length === 2 ? third : third + third
|
|
||||||
|
|
||||||
let last = parseInt(arr.pop()) / 255
|
|
||||||
last = last.toFixed(0)
|
|
||||||
|
|
||||||
let color = {}
|
|
||||||
color['rgba'] = 'rgba(' + arr.join(',') + ',' + last + ')'
|
|
||||||
color['#'] = '#' + first + second + third
|
|
||||||
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
getImgColor.prototype.getColors = function () {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 避免图片过大,阻塞卡死
|
|
||||||
* 每加载一行像素,延迟20毫秒加载下一行
|
|
||||||
* return Promise
|
|
||||||
* promise resolve 解析完成后,返回颜色的总计数组,降序排列
|
|
||||||
* promise reject none
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
let arr = []
|
|
||||||
let getY = (i) => {
|
|
||||||
for (let j = 0; j < this.canvas.height; j++) {
|
|
||||||
let obj = {}
|
|
||||||
obj = this.getColorXY(i, j)
|
|
||||||
obj.index = 1
|
|
||||||
let is = true
|
|
||||||
|
|
||||||
arr.forEach((item) => {
|
|
||||||
if (item['#'] === obj['#']) {
|
|
||||||
is = false
|
|
||||||
item.index += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
let l = []
|
|
||||||
|
|
||||||
for (let i = 0; i < obj['#'].length; i++) {
|
|
||||||
|
|
||||||
if (item['#'].indexOf(obj['#'][i]) > -1) {
|
|
||||||
l.push('1')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let acc = (this.accuracy > 7) ? 7 : this.accuracy
|
|
||||||
acc = (this.accuracy < 1) ? 2 : this.accuracy
|
|
||||||
if (l.length > acc) {
|
|
||||||
is = false
|
|
||||||
item.index += 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (is) {
|
|
||||||
arr.push(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let getX = (i) => {
|
|
||||||
if (i < this.canvas.width) {
|
|
||||||
|
|
||||||
getY(i)
|
|
||||||
this.progress = (i / this.canvas.width * 100).toFixed(2) + '%'
|
|
||||||
console.log(this.progress)
|
|
||||||
setTimeout(() => {
|
|
||||||
getX(++i)
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
this.progress = '100%'
|
|
||||||
console.log(this.progress)
|
|
||||||
|
|
||||||
resolve(arr.sort(function (a, b) {
|
|
||||||
return a.index < b.index ? 1 : (a.index > b.index ? -1 : 0)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getX(0)
|
|
||||||
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*-----------无视这条分割线----------*/
|
|
||||||
/*-----------无视这条分割线----------*/
|
|
||||||
/*-----------无视这条分割线----------*/
|
|
||||||
|
|
||||||
// 实例代码
|
|
||||||
|
|
||||||
let input = document.querySelector("#file")
|
|
||||||
|
|
||||||
input.addEventListener("change", (event) => {
|
|
||||||
/**
|
|
||||||
* 上传图片之后
|
|
||||||
* 替换图片
|
|
||||||
* 执行方法
|
|
||||||
*/
|
|
||||||
let img = document.querySelector("#img")
|
|
||||||
let file = event.target.files[0]
|
|
||||||
let fr = new FileReader()
|
|
||||||
|
|
||||||
fr.onload = (e) => {
|
|
||||||
let n_img = new Image()
|
|
||||||
n_img.src = e.target.result
|
|
||||||
n_img.onload = (e) => {
|
|
||||||
n_img.id = 'img'
|
|
||||||
n_img.width = 300 || n_img.width
|
|
||||||
n_img.height = 300 || n_img.height
|
|
||||||
document.body.replaceChild(n_img, img)
|
|
||||||
getImg()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fr.readAsDataURL(file)
|
|
||||||
})
|
|
||||||
|
|
||||||
let a = null
|
|
||||||
function getImg() {
|
|
||||||
/**
|
|
||||||
* 获取图片,实例化图片
|
|
||||||
* 执行方法
|
|
||||||
* 解析完成,获得数组,操作回调函数
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
let img = document.querySelector("#img")
|
|
||||||
a = new getImgColor(img)
|
|
||||||
|
|
||||||
// 获取 坐标 0 0 点的颜色值
|
|
||||||
console.log(a.getColorXY(100, 100))
|
|
||||||
document.getElementById('img').addEventListener('mousedown', handleSelection, false)
|
|
||||||
return
|
|
||||||
a.getColors().then((arr) => {
|
|
||||||
|
|
||||||
let ul = document.querySelector("#ul")
|
|
||||||
let text = document.querySelector("#text")
|
|
||||||
text.innerText = '共有' + arr.length + '个颜色';
|
|
||||||
let str = ''
|
|
||||||
|
|
||||||
arr.forEach((obj, index) => {
|
|
||||||
str += `<li style="background-color:${obj['#']}">${obj['#']} - ${obj['index']}次</li>`;
|
|
||||||
})
|
|
||||||
|
|
||||||
ul.innerHTML = str
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelection(e) {
|
|
||||||
const r = 1
|
|
||||||
console.log(e.offsetX, e.offsetY, a.getColorXY(e.offsetX * r, e.offsetY * r))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,73 +0,0 @@
|
|||||||
<!--
|
|
||||||
* @Author: ShawnPhang
|
|
||||||
* @Date: 2022-03-14 15:41:17
|
|
||||||
* @Description:
|
|
||||||
* @LastEditors: ShawnPhang
|
|
||||||
* @LastEditTime: 2022-03-14 15:54:24
|
|
||||||
-->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Document</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.out {
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-design-index-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex: 1;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-design-wrap {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-design {
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.out-page {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 60px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.design-canvas {
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
box-shadow: 1px 1px 10px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="page-design-index-wrap">
|
|
||||||
<div class="page-design-wrap out">
|
|
||||||
<div id="page-design" ref="page-design">
|
|
||||||
<div id="out-page" class="out-page">
|
|
||||||
<div class="edit-text" spellcheck="false" contenteditable="plaintext-only">asd</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
x
Reference in New Issue
Block a user