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 特性
|
||||
@ -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** 以上版本
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 拉取源码
|
||||
|
||||
```
|
||||
@ -43,28 +47,22 @@ npm run serve
|
||||
>
|
||||
> 
|
||||
|
||||
### 运行结果
|
||||
|
||||

|
||||
|
||||
合成图片时本地会启动一个 Chrome 浏览器实例。
|
||||
|
||||
### 打包前端页面
|
||||
### 打包
|
||||
|
||||
```
|
||||
npm run v-build
|
||||
```
|
||||
|
||||
### 打包图片生成服务
|
||||
|
||||
```
|
||||
cd sreenshot
|
||||
npm run build
|
||||
npm run v-build <- 前端页面
|
||||
npm run build <- 图片生成服务( sreenshot 目录下)
|
||||
```
|
||||
|
||||
### 服务端
|
||||
|
||||
可参考接口 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
|
||||
* @Description:
|
||||
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||
* @LastEditTime: 2023-07-04 19:04:44
|
||||
* @LastEditTime: 2023-07-18 16:30:53
|
||||
-->
|
||||
# Node截图服务
|
||||
|
||||
@ -15,6 +15,10 @@ ts-node 直接运行,并监听文件修改自动热重启
|
||||
|
||||
使用 webpack 打包文件
|
||||
|
||||
### build:apidoc
|
||||
|
||||
生成 API 文档
|
||||
|
||||
### npm run serve (不使用)
|
||||
|
||||
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