mirror of
https://gitee.com/onvia/ccc-tnt-psd2ui
synced 2026-03-15 10:41:41 +00:00
增加 psd2ui 源码
This commit is contained in:
6
psd2ui-tools/src/psd/LayerType.ts
Normal file
6
psd2ui-tools/src/psd/LayerType.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum LayerType{
|
||||
Doc,
|
||||
Group,
|
||||
Text,
|
||||
Image
|
||||
}
|
||||
46
psd2ui-tools/src/psd/PsdDocument.ts
Normal file
46
psd2ui-tools/src/psd/PsdDocument.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { UIObject } from "../engine/UIObject";
|
||||
import { Rect } from "../values/Rect";
|
||||
import { Size } from "../values/Size";
|
||||
import { PsdGroup } from "./PsdGroup";
|
||||
|
||||
export class PsdDocument extends PsdGroup{
|
||||
|
||||
/** 当前文档所有的图片 */
|
||||
images: Map<string,any> = new Map();
|
||||
|
||||
objectMap: Map<string,number> = new Map();
|
||||
|
||||
objectArray: UIObject[] = [];
|
||||
|
||||
constructor(source: any){
|
||||
super(source,null,null);
|
||||
this.size = new Size(source.width,source.height);
|
||||
this.rect = new Rect(0, this.size.width, 0, this.size.height);
|
||||
}
|
||||
|
||||
pushObject(uiObject: UIObject){
|
||||
let idx = this.objectArray.length;
|
||||
uiObject.idx = idx;
|
||||
this.objectMap.set(uiObject.uuid,idx);
|
||||
this.objectArray.push(uiObject);
|
||||
return idx;
|
||||
}
|
||||
|
||||
getObjectIdx(uuid: string){
|
||||
let idx = this.objectMap.get(uuid);
|
||||
return idx;
|
||||
}
|
||||
getObject(uuid: string){
|
||||
let idx = this.objectMap.get(uuid);
|
||||
if(idx < this.objectArray.length){
|
||||
return this.objectArray[idx];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onCtor(): void {
|
||||
super.onCtor();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
49
psd2ui-tools/src/psd/PsdGroup.ts
Normal file
49
psd2ui-tools/src/psd/PsdGroup.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Rect } from "../values/Rect";
|
||||
import { PsdLayer } from "./PsdLayer";
|
||||
|
||||
export class PsdGroup extends PsdLayer {
|
||||
declare children: PsdLayer[];
|
||||
declare parent: PsdGroup;
|
||||
|
||||
|
||||
constructor(source: any, parent: PsdLayer, rootDoc: PsdLayer) {
|
||||
super(source, parent, rootDoc);
|
||||
this.children = [];
|
||||
if (rootDoc) {
|
||||
this.rect = new Rect(0, rootDoc.size.width, 0, rootDoc.size.height);
|
||||
}
|
||||
}
|
||||
parseSource(): boolean {
|
||||
super.parseSource();
|
||||
|
||||
if(!this.attr?.comps.full){
|
||||
this.resize();
|
||||
this.computeBasePosition();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
resize() {
|
||||
let left = Number.MAX_SAFE_INTEGER;
|
||||
let right = Number.MIN_SAFE_INTEGER;
|
||||
let top = Number.MAX_SAFE_INTEGER;
|
||||
let bottom = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const element = this.children[i];
|
||||
let _rect = element.rect;
|
||||
left = Math.min(_rect.left, left);
|
||||
right = Math.max(_rect.right, right);
|
||||
top = Math.min(_rect.top, top);
|
||||
bottom = Math.max(_rect.bottom, bottom);
|
||||
}
|
||||
this.rect.left = left;
|
||||
this.rect.right = right;
|
||||
this.rect.top = top;
|
||||
this.rect.bottom = bottom;
|
||||
}
|
||||
|
||||
onCtor() {
|
||||
|
||||
}
|
||||
}
|
||||
98
psd2ui-tools/src/psd/PsdImage.ts
Normal file
98
psd2ui-tools/src/psd/PsdImage.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { PsdGroup } from "./PsdGroup";
|
||||
import { PsdLayer } from "./PsdLayer";
|
||||
import { utils } from "../utils/Utils";
|
||||
import canvas from 'canvas';
|
||||
import { Border, Texture9Utils } from "../utils/Texture9Utils";
|
||||
import { Size } from "../values/Size";
|
||||
import { fileUtils } from "../utils/FileUtils";
|
||||
import { Vec3 } from "../values/Vec3";
|
||||
|
||||
export class PsdImage extends PsdLayer {
|
||||
declare parent: PsdGroup;
|
||||
|
||||
declare textureUuid: string;
|
||||
|
||||
declare md5: string;
|
||||
declare imgBuffer: Buffer;
|
||||
|
||||
declare textureSize: Size;
|
||||
|
||||
declare imgName: string;
|
||||
|
||||
|
||||
declare s9: Border;
|
||||
|
||||
constructor(source: any, parent: PsdLayer, rootDoc: PsdLayer) {
|
||||
super(source, parent, rootDoc);
|
||||
this.textureUuid = utils.uuid();
|
||||
|
||||
// img name
|
||||
this.imgName = this.attr.comps.img?.name || this.name
|
||||
|
||||
// .9
|
||||
if (this.attr.comps['.9']) {
|
||||
let s9 = this.attr.comps['.9'];
|
||||
this.s9 = Texture9Utils.safeBorder(this.source.canvas, s9 as any);
|
||||
let newCanvas = Texture9Utils.split(this.source.canvas, s9 as any);
|
||||
this.source.canvas = newCanvas;
|
||||
}
|
||||
let canvas: canvas.Canvas = this.source.canvas;
|
||||
|
||||
this.imgBuffer = canvas.toBuffer('image/png');
|
||||
this.md5 = fileUtils.getMD5(this.imgBuffer);
|
||||
|
||||
this.textureSize = new Size(canvas.width, canvas.height);
|
||||
this.scale = new Vec3((this.isFilpX() ? -1 : 1) * this.scale.x, (this.isFilpY() ? -1 : 1) * this.scale.y, 1);
|
||||
}
|
||||
|
||||
onCtor() {
|
||||
|
||||
}
|
||||
|
||||
isIgnore() {
|
||||
//
|
||||
if (this.attr.comps.ignore || this.attr.comps.ignoreimg) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 是否是镜像图片 */
|
||||
isBind() {
|
||||
return typeof this.attr.comps.flip?.bind !== 'undefined'
|
||||
|| typeof this.attr.comps.img?.bind !== 'undefined';
|
||||
}
|
||||
|
||||
/** 是否是 x 方向镜像图片 */
|
||||
isFilpX() {
|
||||
return typeof this.attr.comps.flipX?.bind !== 'undefined';
|
||||
}
|
||||
|
||||
/** 是否是 y 方向镜像图片 */
|
||||
isFilpY() {
|
||||
return typeof this.attr.comps.flipY?.bind !== 'undefined';
|
||||
}
|
||||
|
||||
// 根据锚点计算坐标
|
||||
updatePositionWithAR() {
|
||||
if (!this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parent = this.parent;
|
||||
while (parent) {
|
||||
this.position.x -= parent.position.x;
|
||||
this.position.y -= parent.position.y;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
// this.position.x = this.position.x - this.parent.size.width * this.parent.anchorPoint.x + this.size.width * this.anchorPoint.x;
|
||||
// this.position.y = this.position.y - this.parent.size.height * this.parent.anchorPoint.y + this.size.height * this.anchorPoint.y;
|
||||
// 如果是镜像图片,则特殊处理
|
||||
let arX = (this.isFilpX() ? (1 - this.anchorPoint.x) : this.anchorPoint.x);
|
||||
let arY = (this.isFilpY() ? (1 - this.anchorPoint.y) : this.anchorPoint.y);
|
||||
this.position.x = this.position.x - this.rootDoc.size.width * this.rootDoc.anchorPoint.x + this.size.width * arX;
|
||||
this.position.y = this.position.y - this.rootDoc.size.height * this.rootDoc.anchorPoint.y + this.size.height * arY;
|
||||
}
|
||||
|
||||
}
|
||||
298
psd2ui-tools/src/psd/PsdLayer.ts
Normal file
298
psd2ui-tools/src/psd/PsdLayer.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { PsdLayerSource } from "../_declare";
|
||||
import { LayerType } from "./LayerType";
|
||||
import { Size } from "../values/Size";
|
||||
import { Vec2 } from "../values/Vec2";
|
||||
import { utils } from "../utils/Utils";
|
||||
import { UIObject } from "../engine/UIObject";
|
||||
import { Rect } from "../values/Rect";
|
||||
import { Color } from "../values/Color";
|
||||
import { Vec3 } from "../values/Vec3";
|
||||
import { pinyin } from "pinyin-pro";
|
||||
|
||||
/**
|
||||
* 命名规则
|
||||
* "name@Type{prop: 1,prop2: 2}"
|
||||
* Type = btn | bar | (toggle @check) | .9 |
|
||||
*
|
||||
*/
|
||||
|
||||
export interface PsdAttr {
|
||||
name: string,
|
||||
comps: {
|
||||
Btn?: {};
|
||||
btn?: {};
|
||||
ProgressBar?: {};
|
||||
progressBar?: {};
|
||||
bar?: {};
|
||||
Toggle?: {};
|
||||
toggle?: {};
|
||||
check?: {};
|
||||
".9"?: { l?: number, r?: number, b?: number, t?: number };
|
||||
ar?: { x?: number, y?: number };
|
||||
// 忽略导出节点和图片
|
||||
ignore?: {};
|
||||
ig?: {};
|
||||
// 忽略导出节点
|
||||
ignorenode?: {};
|
||||
ignode?: {};
|
||||
// 忽略导出图片
|
||||
ignoreimg?: {};
|
||||
igimg?: {};
|
||||
|
||||
full?: {};
|
||||
size?: { w?: number, h?: number };
|
||||
scale?: { x?: number, y?: number };
|
||||
img?: { id?: number, name?: string, bind?: number }
|
||||
flip?: { bind: number, x?: number, y?: number }
|
||||
flipX?: { bind: number }
|
||||
flipY?: { bind: number }
|
||||
// position?:{x?: number,y?: number};
|
||||
// pos?:{x?: number,y?: number};
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class PsdLayer {
|
||||
|
||||
static isPinyin = false;
|
||||
|
||||
declare uuid: string;
|
||||
declare rootDoc: PsdLayer;
|
||||
declare name: string;
|
||||
declare source: PsdLayerSource;
|
||||
declare parent: PsdLayer;
|
||||
declare position: Vec2;
|
||||
declare size: Size;
|
||||
declare rect: Rect;
|
||||
declare anchorPoint: Vec2;
|
||||
declare hidden: boolean;
|
||||
declare opacity: number;
|
||||
declare layerType: LayerType;
|
||||
declare uiObject: UIObject;
|
||||
declare attr: PsdAttr; // 解析名字获得各项属性
|
||||
declare color: Color;
|
||||
declare scale: Vec3;
|
||||
|
||||
constructor(source: any, parent: PsdLayer, rootDoc: PsdLayer) {
|
||||
this.uuid = utils.uuid();
|
||||
this.source = source;
|
||||
this.parent = parent;
|
||||
this.rootDoc = rootDoc;
|
||||
this.name = source.name;
|
||||
this.position = new Vec2();
|
||||
this.size = new Size();
|
||||
this.rect = new Rect(source);
|
||||
// this.anchorPoint = new Vec2();
|
||||
this.anchorPoint = new Vec2(0.5, 0.5);
|
||||
this.hidden = false;
|
||||
this.opacity = 255;
|
||||
this.color = new Color(255, 255, 255, 255);
|
||||
console.log(`PsdLayer->解析到图层 `, this.name);
|
||||
|
||||
this.attr = this.parseNameRule(this.name);
|
||||
// // 更新名字
|
||||
this.name = this.chineseToPinyin(this.attr?.name || this.name);
|
||||
|
||||
// 使用配置的缩放系数
|
||||
let _scale = this.attr?.comps.scale;
|
||||
this.scale = new Vec3(_scale?.x ?? 1, _scale?.y ?? 1, 1);
|
||||
}
|
||||
|
||||
abstract onCtor();
|
||||
|
||||
|
||||
parseNameRule(name: string) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
name = name.trim();
|
||||
let fragments = name.split("@");
|
||||
if (fragments.length === 0) {
|
||||
console.error(`PsdLayer-> 名字解析错误`);
|
||||
return;
|
||||
}
|
||||
let obj: PsdAttr = {
|
||||
name: fragments[0]?.replace(/\.|>|\/|\ /g, "_") ?? "unknow",
|
||||
comps: {},
|
||||
}
|
||||
for (let i = 1; i < fragments.length; i++) {
|
||||
const fragment = fragments[i].trim();
|
||||
let attr = {};
|
||||
let startIdx = fragment.indexOf("{");
|
||||
let comp = fragment;
|
||||
if (startIdx != -1) {
|
||||
|
||||
let endIdx = fragment.indexOf("}");
|
||||
if (endIdx == -1) {
|
||||
console.log(`PsdLayer->${name} 属性 解析错误`);
|
||||
continue;
|
||||
}
|
||||
let attrStr = fragment.substring(startIdx + 1, endIdx);
|
||||
comp = fragment.substr(0, startIdx);
|
||||
attrStr = attrStr.trim();
|
||||
let attrs = attrStr.split(",");
|
||||
attrs.forEach((str) => {
|
||||
str = str.trim();
|
||||
let strs = str.split(":");
|
||||
if (!strs.length) {
|
||||
console.log(`PsdLayer->${name} 属性 解析错误`);
|
||||
return;
|
||||
}
|
||||
strs.map((v) => {
|
||||
return v.trim();
|
||||
});
|
||||
attr[strs[0]] = utils.isNumber(strs[1]) ? parseFloat(strs[1]) : strs[1];
|
||||
});
|
||||
}
|
||||
comp = comp.trim();
|
||||
comp = comp.replace(":", ""); // 防呆,删除 key 中的冒号,
|
||||
obj.comps[comp] = attr;
|
||||
}
|
||||
|
||||
// 获取别名的值
|
||||
obj.comps.ignore = obj.comps.ignore || obj.comps.ig;
|
||||
obj.comps.ignorenode = obj.comps.ignorenode || obj.comps.ignode;
|
||||
obj.comps.ignoreimg = obj.comps.ignoreimg || obj.comps.igimg;
|
||||
obj.comps.Btn = obj.comps.Btn || obj.comps.btn;
|
||||
obj.comps.ProgressBar = obj.comps.ProgressBar || obj.comps.progressBar;
|
||||
obj.comps.Toggle = obj.comps.Toggle || obj.comps.toggle;
|
||||
|
||||
// 图片名中文转拼音
|
||||
if (obj.comps.img) {
|
||||
if (obj.comps.img.name) {
|
||||
obj.comps.img.name = this.chineseToPinyin(obj.comps.img.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 将mirror filpX filpY 进行合并
|
||||
if (obj.comps.flip || obj.comps.flipX || obj.comps.flipY) {
|
||||
obj.comps.flip = Object.assign({}, obj.comps.flip, obj.comps.flipX, obj.comps.flipY);
|
||||
|
||||
if (obj.comps.flipX) {
|
||||
obj.comps.flip.x = 1;
|
||||
}
|
||||
|
||||
if (obj.comps.flipY) {
|
||||
obj.comps.flip.y = 1;
|
||||
}
|
||||
|
||||
// x,y 都缺省时,默认 x 方向镜像
|
||||
if (typeof obj.comps.flip.bind !== 'undefined') {
|
||||
if (!obj.comps.flip.y) {
|
||||
obj.comps.flip.x = 1;
|
||||
}
|
||||
|
||||
// 只有作为镜像图片使用的时候才反向赋值
|
||||
// 反向赋值,防止使用的时候值错误
|
||||
if (obj.comps.flip.x) {
|
||||
obj.comps.flipX = Object.assign({}, obj.comps.flipX, obj.comps.flip);
|
||||
}
|
||||
|
||||
if (obj.comps.flip.y) {
|
||||
obj.comps.flipY = Object.assign({}, obj.comps.flipY, obj.comps.flip);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 检查冲突
|
||||
if (obj.comps.full && obj.comps.size) {
|
||||
console.warn(`PsdLayer->${obj.name} 同时存在 @full 和 @size`);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** 解析数据 */
|
||||
parseSource() {
|
||||
let _source = this.source;
|
||||
|
||||
|
||||
// psd文档
|
||||
if (!this.parent) {
|
||||
return false;
|
||||
}
|
||||
this.hidden = _source.hidden;
|
||||
this.opacity = Math.round(_source.opacity * 255);
|
||||
|
||||
// 获取锚点
|
||||
let ar = this.attr.comps.ar;
|
||||
if (ar) {
|
||||
this.anchorPoint.x = ar.x ?? this.anchorPoint.x;
|
||||
this.anchorPoint.y = ar.y ?? this.anchorPoint.y;
|
||||
}
|
||||
|
||||
this.computeBasePosition();
|
||||
return true;
|
||||
}
|
||||
/** 解析 effect */
|
||||
parseEffects() {
|
||||
// 颜色叠加 暂时搞不定
|
||||
// if(this.source.effects?.solidFill){
|
||||
// let solidFills = this.source.effects?.solidFill;
|
||||
// for (let i = 0; i < solidFills.length; i++) {
|
||||
// const solidFill = solidFills[i];
|
||||
// if(solidFill.enabled){
|
||||
// let color = solidFill.color;
|
||||
// this.color = new Color(color.r,color.g,color.b,solidFill.opacity * 255);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/** 中文转拼音 */
|
||||
chineseToPinyin(text: string) {
|
||||
if (!text || !PsdLayer.isPinyin) {
|
||||
return text;
|
||||
}
|
||||
let names = pinyin(text, {
|
||||
toneType: "none",
|
||||
type: "array"
|
||||
});
|
||||
|
||||
names = names.map((text) => {
|
||||
return text.slice(0, 1).toUpperCase() + text.slice(1).toLowerCase();
|
||||
});
|
||||
return names.join("");
|
||||
}
|
||||
|
||||
// 计算初始坐标 左下角 0,0 为锚点
|
||||
computeBasePosition() {
|
||||
if (!this.rootDoc) {
|
||||
return;
|
||||
}
|
||||
let _rect = this.rect;
|
||||
let width = (_rect.right - _rect.left);
|
||||
let height = (_rect.bottom - _rect.top);
|
||||
|
||||
this.size.width = width;
|
||||
this.size.height = height;
|
||||
|
||||
// 位置 左下角为锚点
|
||||
let x = _rect.left;
|
||||
let y = (this.rootDoc.size.height - _rect.bottom);
|
||||
|
||||
this.position.x = x;
|
||||
this.position.y = y;
|
||||
}
|
||||
|
||||
// 根据锚点计算坐标
|
||||
updatePositionWithAR() {
|
||||
if (!this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parent = this.parent;
|
||||
while (parent) {
|
||||
this.position.x -= parent.position.x;
|
||||
this.position.y -= parent.position.y;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
// this.position.x = this.position.x - this.parent.size.width * this.parent.anchorPoint.x + this.size.width * this.anchorPoint.x;
|
||||
// this.position.y = this.position.y - this.parent.size.height * this.parent.anchorPoint.y + this.size.height * this.anchorPoint.y;
|
||||
this.position.x = this.position.x - this.rootDoc.size.width * this.rootDoc.anchorPoint.x + this.size.width * this.anchorPoint.x;
|
||||
this.position.y = this.position.y - this.rootDoc.size.height * this.rootDoc.anchorPoint.y + this.size.height * this.anchorPoint.y;
|
||||
}
|
||||
|
||||
}
|
||||
67
psd2ui-tools/src/psd/PsdText.ts
Normal file
67
psd2ui-tools/src/psd/PsdText.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
import { config } from "../config";
|
||||
import { Color } from "../values/Color";
|
||||
import { Vec2 } from "../values/Vec2";
|
||||
import { PsdGroup } from "./PsdGroup";
|
||||
import { PsdLayer } from "./PsdLayer";
|
||||
|
||||
export class PsdText extends PsdLayer{
|
||||
declare parent: PsdGroup;
|
||||
declare text: string;
|
||||
declare fontSize: number;
|
||||
declare font: string;
|
||||
declare outline: { width: number, color: Color }; // 描边
|
||||
declare offsetY: number;
|
||||
|
||||
|
||||
parseSource(): boolean {
|
||||
super.parseSource();
|
||||
let textSource = this.source.text;
|
||||
let style = textSource.style;
|
||||
if(style){
|
||||
let fillColor = style.fillColor;
|
||||
if(fillColor){
|
||||
this.color = new Color(fillColor.r,fillColor.g,fillColor.b,fillColor.a * 255);
|
||||
}
|
||||
}
|
||||
this.text = textSource.text;
|
||||
this.fontSize = style.fontSize;
|
||||
|
||||
this.offsetY = config.textOffsetY[this.fontSize] || config.textOffsetY["default"] || 0;
|
||||
|
||||
this.parseSolidFill();
|
||||
this.parseStroke();
|
||||
return true;
|
||||
}
|
||||
onCtor(){
|
||||
|
||||
}
|
||||
|
||||
/** 描边 */
|
||||
parseStroke(){
|
||||
if(this.source.effects?.stroke){
|
||||
let stroke = this.source.effects?.stroke[0];
|
||||
// 外描边
|
||||
if(stroke?.enabled && stroke?.position === "outside"){
|
||||
let color = stroke.color;
|
||||
this.outline = {
|
||||
width: stroke.size.value,
|
||||
color: new Color(color.r,color.g,color.b,stroke.opacity * 255)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 解析 颜色叠加 */
|
||||
parseSolidFill() {
|
||||
if(this.source.effects?.solidFill){
|
||||
let solidFills = this.source.effects?.solidFill;
|
||||
for (let i = 0; i < solidFills.length; i++) {
|
||||
const solidFill = solidFills[i];
|
||||
if(solidFill.enabled){
|
||||
let color = solidFill.color;
|
||||
this.color = new Color(color.r,color.g,color.b,solidFill.opacity * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user