mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
v0.0.1
This commit is contained in:
parent
581378c44b
commit
cde0003c50
14 changed files with 7372 additions and 2 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
dist
|
123
README.md
123
README.md
|
@ -1,2 +1,121 @@
|
||||||
# penpot-exporter-figma-plugin
|
|
||||||
Penpot exporter Figma plugin
|
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
|
||||||
|
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
|
||||||
|
|
||||||
|
<h1 align="center">
|
||||||
|
<br>
|
||||||
|
<img src="https://penpot.app/images/readme/readme-logo.jpg" alt="PENPOT">
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||||
|
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||||
|
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://penpot.app/"><b>Penpot Website</b></a> •
|
||||||
|
<a href="https://community.penpot.app/t/figma-file-importer/1684"><b>Figma importer (penpot community)</b></a> •
|
||||||
|
<a href="https://community.penpot.app/"><b>Penpot Community</b></a> •
|
||||||
|
</p>
|
||||||
|
|
||||||
|
![](penpotexporter.gif)
|
||||||
|
|
||||||
|
This is a **very early stage** figma plugin to export figma files to Penpot format. For now is little more than a proof of concept, or a first scaffolding, not a fully functional exporter.
|
||||||
|
|
||||||
|
## Table of contents ##
|
||||||
|
|
||||||
|
- [Why a figma exporter](#why-a-figma-exporter)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Call to the community](#call-to-the-community)
|
||||||
|
- [What can this plugin currently import?](#what-can-this-plugin-currently-import)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Limitations](#limitations)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Why a figma exporter ##
|
||||||
|
|
||||||
|
The aim of this plugin is to help people migrate their files from Figma to [Penpot](https://penpot.app/). Migrating work from a design tool to another was never an easy task due to the abundance of closed and non-standard formats, and this is not a different case. Our approach to better solve this situation is to release a code skeleton for the minimum version of a Figma plugin that can convert a Figma file into a Penpot annotated svg file.
|
||||||
|
|
||||||
|
There is a sense of urgency for this capability because there is a feeling that Adobe might force Figma to limit exports and interoperability via plugins very soon.
|
||||||
|
|
||||||
|
|
||||||
|
## Getting started ##
|
||||||
|
|
||||||
|
This plugin makes use of npm, webpack and react, and is written on TypeScript. It halso includes a penpot file builder library.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
* node / npm
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the plugin
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add the plugin to Figma
|
||||||
|
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…`
|
||||||
|
|
||||||
|
And choose the manifest.json file
|
||||||
|
|
||||||
|
### Launch the plugin and exporting a penpot file
|
||||||
|
`Figma menu` > `Plugins` > `Development` > `Penpot Exporter`
|
||||||
|
|
||||||
|
This will generate a .zip file that you can import from penpot
|
||||||
|
|
||||||
|
## Call to the community ##
|
||||||
|
|
||||||
|
Answering to the interest expressed by community members to build the plugin by themselves, at the Penpot team we decided to help solve the need without having to depend on our current product priorities. That is why we have published this bare minimum version of the plugin, unsatisfactory in itself, but it unlocks the possibility for others to continue the task.
|
||||||
|
|
||||||
|
Yes, we are asking for help. 🤗
|
||||||
|
|
||||||
|
We have explained this approach in a [community post](https://community.penpot.app/t/figma-file-importer/1684). Feel free to join the conversation there.
|
||||||
|
|
||||||
|
## What can this plugin currently import? ##
|
||||||
|
|
||||||
|
As mentioned above, this plugin gets you to a starting point. Things that are currently included in the import are:
|
||||||
|
|
||||||
|
- **Basic shapes** (rectangles, ellipses).
|
||||||
|
- **Frames** (Boards in Penpot).
|
||||||
|
- **Groups**.
|
||||||
|
- **Fills** (solid colors and linear gradients).
|
||||||
|
- **Fonts** (only if they are available on Google fonts).
|
||||||
|
|
||||||
|
## Limitations ##
|
||||||
|
The obvious limitations are the features that are in Figma but not in Penpot or work differently in both tools so they can not be easily converted. We leave some comments below about the ones that are commonly considered more important:
|
||||||
|
|
||||||
|
- **Autolayout**: Not in Penpot yet but in a very advanced state of development. There will be news soon.
|
||||||
|
- **Components**: Currently very different from their counterparts at Figma. However, Penpot components are under a rework that we expect will make the conversion easier.
|
||||||
|
- **Variants**: Not expected in the short term. Also, we are thinking of different solutions to solve component states, things that eventually could make it difficult to import.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing ##
|
||||||
|
|
||||||
|
If you want to make many people very happy and help us build this code skeleton for the minimum version of the Figma plugin, further effort will be needed to have a satisfactory import experience.
|
||||||
|
|
||||||
|
For instance, it will be interesting to add:
|
||||||
|
- Strokes
|
||||||
|
- Fills with radial gradients
|
||||||
|
- Paths
|
||||||
|
- Images
|
||||||
|
- Comments
|
||||||
|
- Rotations
|
||||||
|
- Constraints
|
||||||
|
- ...
|
||||||
|
|
||||||
|
Motivated to contribute? Take a look at our [Contributing Guide](https://help.penpot.app/contributing-guide/) that explains our guidelines (they're for the Penpot Core, but are mostly of application here too).
|
||||||
|
|
||||||
|
|
||||||
|
## License ##
|
||||||
|
|
||||||
|
```
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
Copyright (c) KALEIDOS INC
|
||||||
|
```
|
||||||
|
Penpot and the Penpot exporter plugin are Kaleidos’ [open source projects](https://kaleidos.net/products)
|
||||||
|
|
8
manifest.json
Normal file
8
manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Penpot Exporter",
|
||||||
|
"id": "",
|
||||||
|
"api": "1.0.0",
|
||||||
|
"main": "dist/code.js",
|
||||||
|
"ui": "dist/ui.html",
|
||||||
|
"editorType": ["figma", "figjam"]
|
||||||
|
}
|
38
package.json
Normal file
38
package.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "penpot-exporter",
|
||||||
|
"id": "1161652283781700708",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Penpot exporter",
|
||||||
|
"main": "code.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "webpack",
|
||||||
|
"watch": "webpack watch"
|
||||||
|
},
|
||||||
|
"author": "Kaleidos",
|
||||||
|
"license": "MPL2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@figma/plugin-typings": "*",
|
||||||
|
"@types/node": "^16.11.64",
|
||||||
|
"css-loader": "^6.2.0",
|
||||||
|
"html-webpack-inline-source-plugin": "0.0.10",
|
||||||
|
"html-webpack-plugin": "^5.3.2",
|
||||||
|
"style-loader": "^3.2.1",
|
||||||
|
"ts-loader": "^9.2.5",
|
||||||
|
"typescript": "^4.3.5",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.51.1",
|
||||||
|
"webpack-cli": "^4.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@figma-plugin/helpers": "^0.15.2",
|
||||||
|
"@types/react": "^17.0.19",
|
||||||
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"crypto-browserify": "^3.12.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dev-utils": "^11.0.4",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"slugify": "^1.6.5"
|
||||||
|
}
|
||||||
|
}
|
BIN
penpotexporter.gif
Normal file
BIN
penpotexporter.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 374 KiB |
3
src/1logo.svg
Normal file
3
src/1logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M27 5V27H5M31 9V31H9M1 1H23V23H1V1Z" stroke="black" stroke-width="2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 185 B |
108
src/code.ts
Normal file
108
src/code.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
|
||||||
|
|
||||||
|
let fileData = [];
|
||||||
|
|
||||||
|
type NodeData = {
|
||||||
|
id: string
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
children: Node[],
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
fills: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatures = {
|
||||||
|
R0lGODdh: "image/gif",
|
||||||
|
R0lGODlh: "image/gif",
|
||||||
|
iVBORw0KGgo: "image/png",
|
||||||
|
"/9j/": "image/jpg"
|
||||||
|
};
|
||||||
|
|
||||||
|
function detectMimeType(b64) {
|
||||||
|
for (var s in signatures) {
|
||||||
|
if (b64.indexOf(s) === 0) {
|
||||||
|
return signatures[s];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function traverse(node): NodeData {
|
||||||
|
let children:any[] = [];
|
||||||
|
|
||||||
|
if ("children" in node) {
|
||||||
|
if (node.type !== "INSTANCE") {
|
||||||
|
for (const child of node.children) {
|
||||||
|
children.push (traverse(child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = {
|
||||||
|
id: node.id,
|
||||||
|
type: node.type,
|
||||||
|
name: node.name,
|
||||||
|
children: children,
|
||||||
|
x: node.x,
|
||||||
|
y: node.y,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
fills: node.fills
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.fills){
|
||||||
|
for (const paint of node.fills) {
|
||||||
|
if (paint.type === 'IMAGE') {
|
||||||
|
// Get the (encoded) bytes for this image.
|
||||||
|
const image = figma.getImageByHash(paint.imageHash);
|
||||||
|
image.getBytesAsync().then((value) => {
|
||||||
|
const b64 = figma.base64Encode(value);
|
||||||
|
figma.ui.postMessage({type: "IMAGE", data: {
|
||||||
|
imageHash: paint.imageHash,
|
||||||
|
value: "data:" + detectMimeType(b64) + ";base64," + b64
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type == "TEXT") {
|
||||||
|
|
||||||
|
let font = {
|
||||||
|
fontName: node.fontName,
|
||||||
|
fontSize: node.fontSize,
|
||||||
|
fontWeight: node.fontWeight,
|
||||||
|
characters: node.characters,
|
||||||
|
lineHeight: node.lineHeight,
|
||||||
|
letterSpacing: node.letterSpacing,
|
||||||
|
textAlignHorizontal: node.textAlignHorizontal,
|
||||||
|
textAlignVertical: node.textAlignVertical
|
||||||
|
};
|
||||||
|
|
||||||
|
result = {...result, ...font};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
|
||||||
|
|
||||||
|
let root: NodeData = traverse(figma.root) // start the traversal at the root
|
||||||
|
figma.ui.postMessage({type: "FIGMAFILE", data: root});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
figma.ui.onmessage = (msg) => {
|
||||||
|
if (msg.type === "cancel") {
|
||||||
|
figma.closePlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "resize") {
|
||||||
|
figma.ui.resize(msg.width, msg.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg height="500" viewBox="0 0 500 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m159.4607 552.36219-52.57675 74.05348v41.86098l-45.753774 21.76184-.412151-.19478v17.20283 255.89707l178.379885 84.27239 10.90209 5.1462 10.89926-5.1462 178.38271-84.27239v-273.0999l-.33593.15808-45.76789-21.76749v-41.81863l-1.60059-2.25268-50.97899-71.8008-52.57958 74.05348v.0734l-38.25894-53.88377-37.96254 53.4688-1.35782-1.91111zm9.3015 43.01555 20.33627 28.64128h-59.27553l20.09914-28.30535zm181.13787 0 20.33626 28.64128h-59.27553l20.09632-28.30535zm-90.83852 20.24593 20.33626 28.63846h-59.2727l20.09631-28.30535zm-134.85903 22.82891h28.11339v94.66356l-28.11339-13.2818zm42.54695 0h27.97224l-.003 114.69495-27.97224-13.21405zm138.58809 0h28.11622l-.003 101.38492-28.11057 13.27898-.003-114.6639zm42.54695 0h27.97224v81.3507l-27.97224 13.21406zm-133.38265 20.24311h28.11339v117.07749l-28.11339-13.2818zm42.54695 0h27.97225l-.003 104.02152-27.97224 13.21688.003-117.2384zm136.12651 31.11133 24.75131 10.12014-24.75131 11.6925zm-286.29137.0367v21.80982l-24.748483-11.6925zm-24.367392 34.4113 156.581352 73.96879v224.87888l-156.581352-73.96877zm334.964042 0v224.8789l-156.58134 73.96877v-224.87888z" transform="translate(0 -552.3622)"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
6571
src/penpot.js
Normal file
6571
src/penpot.js
Normal file
File diff suppressed because it is too large
Load diff
105
src/ui.css
Normal file
105
src/ui.css
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
:root {
|
||||||
|
--color-bg: var(--figma-color-bg);
|
||||||
|
--color-bg-hover: var(--figma-color-bg-hover);
|
||||||
|
--color-bg-active: var(--figma-color-bg-pressed);
|
||||||
|
--color-border: var(--figma-color-border);
|
||||||
|
--color-border-focus: var(--figma-color-border-selected);
|
||||||
|
--color-icon: var(--figma-color-icon);
|
||||||
|
--color-text: var(--figma-color-text);
|
||||||
|
--color-bg-brand: var(--figma-color-bg-brand);
|
||||||
|
--color-bg-brand-hover: var(--figma-color-bg-brand-hover);
|
||||||
|
--color-bg-brand-active: var(--figma-color-bg-brand-pressed);
|
||||||
|
--color-border-brand: var(--figma-color-border-brand);
|
||||||
|
--color-border-brand-focus: var(--figma-color-border-selected-strong);
|
||||||
|
--color-text-brand: var(--figma-color-text-onbrand);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
background-color: var(--color-bg-active);
|
||||||
|
}
|
||||||
|
button:focus-visible {
|
||||||
|
border: none;
|
||||||
|
outline-color: var(--color-border-focus);
|
||||||
|
}
|
||||||
|
button.brand {
|
||||||
|
--color-bg: var(--color-bg-brand);
|
||||||
|
--color-text: var(--color-text-brand);
|
||||||
|
--color-bg-hover: var(--color-bg-brand-hover);
|
||||||
|
--color-bg-active: var(--color-bg-brand-active);
|
||||||
|
--color-border: transparent;
|
||||||
|
--color-border-focus: var(--color-border-brand-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: 1px solid var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: 1px solid var(--color-text);
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible {
|
||||||
|
border-color: var(--color-border-focus);
|
||||||
|
outline-color: var(--color-border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
stroke: var(--color-icon, rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
section > * + * {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
footer > * + * {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: auto;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
1
src/ui.html
Normal file
1
src/ui.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div id="penpot-export-page"></div>
|
337
src/ui.tsx
Normal file
337
src/ui.tsx
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import "./ui.css";
|
||||||
|
import * as penpot from "./penpot.js";
|
||||||
|
|
||||||
|
import { extractLinearGradientParamsFromTransform } from "@figma-plugin/helpers";
|
||||||
|
import slugify from "slugify";
|
||||||
|
|
||||||
|
declare function require(path: string): any;
|
||||||
|
|
||||||
|
|
||||||
|
type PenpotExporterProps = {
|
||||||
|
}
|
||||||
|
|
||||||
|
type PenpotExporterState = {
|
||||||
|
isDebug: boolean,
|
||||||
|
penpotFileData: string
|
||||||
|
figmaFileData: string
|
||||||
|
figmaRootNode: NodeData
|
||||||
|
images: { [id: string] : string; };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PenpotExporter extends React.Component<PenpotExporterProps, PenpotExporterState> {
|
||||||
|
state: PenpotExporterState = {
|
||||||
|
isDebug: false,
|
||||||
|
penpotFileData: "",
|
||||||
|
figmaFileData: "",
|
||||||
|
figmaRootNode: null,
|
||||||
|
images: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
window.addEventListener("message", this.onMessage);
|
||||||
|
}
|
||||||
|
componentWillUnmount = () =>{
|
||||||
|
window.removeEventListener('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
rgbToHex = (color) => {
|
||||||
|
const r = Math.round(255 * color.r);
|
||||||
|
const g = Math.round(255 * color.g);
|
||||||
|
const b = Math.round(255 * color.b);
|
||||||
|
const rgb = (r << 16) | (g << 8) | (b << 0);
|
||||||
|
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateSolidFill(fill){
|
||||||
|
return {
|
||||||
|
fillColor: this.rgbToHex(fill.color),
|
||||||
|
fillOpacity: fill.opacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
translateGradientLinearFill(fill, width, height){
|
||||||
|
const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
||||||
|
return {
|
||||||
|
fillColorGradient: {
|
||||||
|
type: Symbol.for("linear"),
|
||||||
|
width: 1,
|
||||||
|
startX: points.start[0] / width,
|
||||||
|
startY: points.start[1] / height,
|
||||||
|
endX: points.end[0] / width,
|
||||||
|
endY: points.end[1] / height,
|
||||||
|
stops: [{
|
||||||
|
color: this.rgbToHex(fill.gradientStops[0].color),
|
||||||
|
offset: fill.gradientStops[0].position,
|
||||||
|
opacity: fill.gradientStops[0].color.a * fill.opacity
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: this.rgbToHex(fill.gradientStops[1].color),
|
||||||
|
offset: fill.gradientStops[1].position,
|
||||||
|
opacity: fill.gradientStops[1].color.a * fill.opacity
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
translateFill(fill, width, height){
|
||||||
|
if (fill.type === "SOLID"){
|
||||||
|
return this.translateSolidFill(fill);
|
||||||
|
} else if (fill.type === "GRADIENT_LINEAR"){
|
||||||
|
return this.translateGradientLinearFill(fill, width, height);
|
||||||
|
} else {
|
||||||
|
console.error('Color type '+fill.type+' not supported yet');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
translateFills(fills, width, height){
|
||||||
|
let penpotFills = [];
|
||||||
|
let penpotFill = null;
|
||||||
|
for (var fill of fills){
|
||||||
|
penpotFill = this.translateFill(fill, width, height);
|
||||||
|
if (penpotFill !== null){
|
||||||
|
penpotFills.unshift(penpotFill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return penpotFills;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotPage(file, node){
|
||||||
|
file.addPage(node.name);
|
||||||
|
for (var child of node.children){
|
||||||
|
this.createPenpotItem(file, child, 0, 0);
|
||||||
|
if (child.fills) {
|
||||||
|
for (var fill of child.fills ){
|
||||||
|
if (fill.type === "IMAGE"){
|
||||||
|
this.createPenpotImage(file, child, 0, 0, this.state.images[fill.imageHash]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.closePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotBoard(file, node, baseX, baseY){
|
||||||
|
file.addArtboard({ name: node.name, x: node.x - baseX, y: node.y - baseY, width: node.width, height: node.height });
|
||||||
|
for (var child of node.children){
|
||||||
|
this.createPenpotItem(file, child, node.x - baseX, node.y - baseY);
|
||||||
|
}
|
||||||
|
file.closeArtboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotGroup(file, node, baseX, baseY){
|
||||||
|
file.addGroup({name: node.name});
|
||||||
|
for (var child of node.children){
|
||||||
|
this.createPenpotItem(file, child, baseX, baseY);
|
||||||
|
}
|
||||||
|
file.closeGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotRectangle(file, node, baseX, baseY){
|
||||||
|
file.createRect({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
|
||||||
|
fills: this.translateFills(node.fills, node.width, node.height),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotCircle(file, node, baseX, baseY){
|
||||||
|
file.createCircle({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
|
||||||
|
fills: this.translateFills(node.fills, node.width, node.height),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
translateHorizontalAlign(align: string){
|
||||||
|
if (align === "RIGHT"){
|
||||||
|
return Symbol.for("right");
|
||||||
|
}
|
||||||
|
if (align === "CENTER"){
|
||||||
|
return Symbol.for("center");
|
||||||
|
}
|
||||||
|
return Symbol.for("left")
|
||||||
|
}
|
||||||
|
|
||||||
|
translateVerticalAlign(align: string){
|
||||||
|
if (align === "BOTTOM"){
|
||||||
|
return Symbol.for("bottom");
|
||||||
|
}
|
||||||
|
if (align === "CENTER"){
|
||||||
|
return Symbol.for("center");
|
||||||
|
}
|
||||||
|
return Symbol.for("top")
|
||||||
|
}
|
||||||
|
|
||||||
|
translateFontStyle(style:string){
|
||||||
|
return style.toLowerCase().replace(/\s/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotText(file, node, baseX, baseY){
|
||||||
|
file.createText({
|
||||||
|
name: node.name,
|
||||||
|
x: node.x + baseX,
|
||||||
|
y: node.y + baseY,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
rotation: 0,
|
||||||
|
type: Symbol.for("text"),
|
||||||
|
content: {
|
||||||
|
type: "root",
|
||||||
|
verticalAlign: this.translateVerticalAlign(node.textAlignVertical),
|
||||||
|
children: [{
|
||||||
|
type: "paragraph-set",
|
||||||
|
children: [{
|
||||||
|
lineHeight: node.lineHeight,
|
||||||
|
fontStyle: "normal",
|
||||||
|
children: [{
|
||||||
|
lineHeight: node.lineHeight,
|
||||||
|
fontStyle: "normal",
|
||||||
|
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
||||||
|
fontId: "gfont-" + slugify(node.fontName.family.toLowerCase()),
|
||||||
|
fontSize: node.fontSize,
|
||||||
|
fontWeight: node.fontWeight,
|
||||||
|
fontVariantId: this.translateFontStyle(node.fontName.style),
|
||||||
|
textDecoration: "none",
|
||||||
|
letterSpacing: node.letterSpacing,
|
||||||
|
fills: this.translateFills(node.fills, node.width, node.height),
|
||||||
|
fontFamily: node.fontName.family,
|
||||||
|
text: node.characters
|
||||||
|
}],
|
||||||
|
textTransform: "none",
|
||||||
|
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
||||||
|
fontId: "gfont-" + slugify(node.fontName.family.toLowerCase()),
|
||||||
|
fontSize: node.fontSize,
|
||||||
|
fontWeight: node.fontWeight,
|
||||||
|
type: "paragraph",
|
||||||
|
textDecoration: "none",
|
||||||
|
letterSpacing: node.letterSpacing,
|
||||||
|
fills: this.translateFills(node.fills, node.width, node.height),
|
||||||
|
fontFamily: node.fontName.family
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotImage(file, node, baseX, baseY, image){
|
||||||
|
file.createImage({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
|
||||||
|
metadata: {
|
||||||
|
width: node.width,
|
||||||
|
height: node.height
|
||||||
|
},
|
||||||
|
dataUri: image
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotItem(file, node, baseX, baseY){
|
||||||
|
if (node.type == "PAGE"){
|
||||||
|
this.createPenpotPage(file, node);
|
||||||
|
}
|
||||||
|
else if (node.type == "FRAME"){
|
||||||
|
this.createPenpotBoard(file, node, baseX, baseY);
|
||||||
|
}
|
||||||
|
else if (node.type == "GROUP"){
|
||||||
|
this.createPenpotGroup(file, node,baseX, baseY);
|
||||||
|
}
|
||||||
|
else if (node.type == "RECTANGLE"){
|
||||||
|
this.createPenpotRectangle(file, node, baseX, baseY);
|
||||||
|
}
|
||||||
|
else if (node.type == "ELLIPSE"){
|
||||||
|
this.createPenpotCircle(file, node, baseX, baseY);
|
||||||
|
}
|
||||||
|
else if (node.type == "TEXT"){
|
||||||
|
this.createPenpotText(file, node, baseX, baseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createPenpotFile(){
|
||||||
|
let node = this.state.figmaRootNode;
|
||||||
|
const file = penpot.createFile(node.name);
|
||||||
|
for (var page of node.children){
|
||||||
|
this.createPenpotItem(file, page, 0, 0);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreatePenpot = () => {
|
||||||
|
const file = this.createPenpotFile();
|
||||||
|
const penpotFileMap = file.asMap();
|
||||||
|
this.setState(state => ({penpotFileData: JSON.stringify(penpotFileMap, (key, value) => (value instanceof Map ? [...value] : value), 4)}));
|
||||||
|
file.export()
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel = () => {
|
||||||
|
parent.postMessage({ pluginMessage: { type: "cancel" } }, "*");
|
||||||
|
};
|
||||||
|
|
||||||
|
onMessage = (event) => {
|
||||||
|
if (event.data.pluginMessage.type == "FIGMAFILE") {
|
||||||
|
this.setState(state => ({
|
||||||
|
figmaFileData: JSON.stringify(event.data.pluginMessage.data, (key, value) => (value instanceof Map ? [...value] : value), 4),
|
||||||
|
figmaRootNode: event.data.pluginMessage.data}));
|
||||||
|
}
|
||||||
|
else if (event.data.pluginMessage.type == "IMAGE") {
|
||||||
|
const data = event.data.pluginMessage.data;
|
||||||
|
this.setState(state =>
|
||||||
|
{
|
||||||
|
state.images[data.imageHash] = data.value;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
) ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDebug = (event) => {
|
||||||
|
console.log(event);
|
||||||
|
console.log(event.currentTarget);
|
||||||
|
console.log(event.currentTarget.checked);
|
||||||
|
const isDebug = event.currentTarget.checked;
|
||||||
|
this.setState (state => ({isDebug: isDebug}))
|
||||||
|
if (isDebug){
|
||||||
|
parent.postMessage({ pluginMessage: { type: "resize", width:800, height:800 } }, "*");
|
||||||
|
} else {
|
||||||
|
parent.postMessage({ pluginMessage: { type: "resize", width:300, height:200 } }, "*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<img src={require("./logo.svg")} />
|
||||||
|
<h2>Penpot Exporter</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="chkDebug" name="chkDebug" onChange={this.toggleDebug}/>
|
||||||
|
<label htmlFor="chkDebug">Show debug data</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div style={{display: this.state.isDebug? '': 'none'}}>
|
||||||
|
<section>
|
||||||
|
<textarea style={{width:'790px', height:'270px'}} id="figma-file-data" value={this.state.figmaFileData} readOnly />
|
||||||
|
<label htmlFor="figma-file-data">Figma file data</label>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<textarea style={{width:'790px', height:'270px'}} id="penpot-file-data" value={this.state.penpotFileData} readOnly />
|
||||||
|
<label htmlFor="penpot-file-data">Penpot file data</label>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button className="brand" onClick={this.onCreatePenpot}>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
<button onClick={this.onCancel}>Cancel</button>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<PenpotExporter />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('penpot-export-page')
|
||||||
|
);
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"jsx": "react",
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@figma"
|
||||||
|
],
|
||||||
|
"moduleResolution":"node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
|
|
63
webpack.config.js
Normal file
63
webpack.config.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
module.exports = (env, argv) => ({
|
||||||
|
mode: argv.mode === 'production' ? 'production' : 'development',
|
||||||
|
|
||||||
|
// This is necessary because Figma's 'eval' works differently than normal eval
|
||||||
|
devtool: argv.mode === 'production' ? false : 'inline-source-map',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
ui: './src/ui.tsx', // The entry point for your UI code
|
||||||
|
code: './src/code.ts', // The entry point for your plugin code
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
// Converts TypeScript code to JavaScript
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enables including CSS by doing "import './file.css'" in your TypeScript code
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ["style-loader", "css-loader"],
|
||||||
|
},
|
||||||
|
// Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI
|
||||||
|
// { test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: [{ loader: 'url-loader' }] }
|
||||||
|
{
|
||||||
|
test: /\.svg/,
|
||||||
|
type: 'asset/inline'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Webpack tries these extensions for you if you omit the extension like "import './file'"
|
||||||
|
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'] ,
|
||||||
|
fallback: { "crypto": false }},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
filename: '[name].js',
|
||||||
|
path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tells Webpack to generate "ui.html" and to inline "ui.ts" into it
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'global': {} // Fix missing symbol error when running in developer VM
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
inject: "body",
|
||||||
|
template: './src/ui.html',
|
||||||
|
filename: 'ui.html',
|
||||||
|
chunks: ['ui']
|
||||||
|
}),
|
||||||
|
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]),
|
||||||
|
],
|
||||||
|
})
|
Loading…
Reference in a new issue