Add web frontend panel skeleton & Vue environment

This commit is contained in:
Keavon Chambers 2021-02-16 03:21:15 -08:00
parent 0578e8f7c7
commit 0a0628202f
33 changed files with 13929 additions and 63 deletions

38
.vscode/settings.json vendored
View file

@ -3,13 +3,37 @@
"editor.formatOnSave": true,
"editor.formatOnPaste": true
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true,
},
"[javascript]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true,
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true,
},
"rust-analyzer.rustfmt.extraArgs": [
"--config", // Remove when rustfmt 2.0
"match_block_trailing_comma=true", // is released
"--config", // Remove when control_brace_style
"control_brace_style=ClosingNextLine" // becomes stable https://github.com/rust-lang/rustfmt/issues/3377
"--config", // Remove when rustfmt
"match_block_trailing_comma=true", // 2.0 is released
"--config", // Remove when `control_brace_style` becomes stable
"control_brace_style=ClosingNextLine" // https://github.com/rust-lang/rustfmt/issues/3377
],
"rust-analyzer.diagnostics.disabled": [ // Remove when rust-analyzer bug fixes
"missing-unsafe" // unsafe code on WASM JavaScript
] // https://github.com/rust-analyzer/rust-analyzer/issues/5412
"rust-analyzer.diagnostics.disabled": [
"missing-unsafe" // Remove when rust-analyzer bug fixes unsafe code on WASM JavaScript https://github.com/rust-analyzer/rust-analyzer/issues/5412
],
"vetur.format.options.useTabs": true,
"eslint.format.enable": true,
"files.eol": "\n",
"vetur.validation.interpolation": false,
"vetur.format.options.tabSize": 4,
"vetur.experimental.templateInterpolationService": true
}

View file

@ -1,28 +1,55 @@
# What is Graphite?
Graphite is an open-source, cross-platform digital content creation desktop app for 2D graphics editing, photo processing, vector art, digital painting, illustration, and compositing. Inspired by the open-source success story of Blender in the 3D domain, it aims to bring 2D content creation to new heights with efficient workflows influenced by Photoshop and Illustrator and backed by a powerful node-based, nondestructive approach proven by Houdini and Substance. The user experience of Graphite is of central importance, offering a meticulously-designed UI catering towards an intuitive and efficient artistic process. Users may draw and edit in the traditional interactive (WYSIWYG) viewport or jump in or out of the node graph at any time to tweak previous work and construct powerful procedural image generators that seamlessly sync with the interactive viewport. A core principle of the application is its 100% nondestructive workflow that is resolution-agnostic, meaning that raster-style image editing can be infinitely zoomed and scaled to any arbitrary resolution at a later time because editing is done by recording brush strokes, vector shapes, and other manipulations parametrically. One might use the painting tools on a small laptop display, zoom into specific areas to add detail to finish the artwork, then perhaps try changing the simulated brush style from a blunt pencil to a soft acrylic paintbrush after-the-fact, and finally export the complete drawing at ultra high resolution for printing on a giant poster. On the surface, Graphite is an artistic medium for drawing anything imaginable— under the hood, the node graph in Graphite powers procedural graphics and parametric rendering to produce unique artwork and automated data-driven visualizations. Graphite brings together artistic workflows and empowers your creativity in a free, open-source package that feels familiar but lets you go further.
## Status
*The code has not had many updates in the past half year as I moved my focus towards hammering out more of the product design and user experience. I'm looking forward to working with interested community members to get back to developing the code further throughout 2021, with the goal of establishing a minimum viable product by end-of-year.*
Graphite is in an early stage of development and its vision is highly ambitious. The project is seeking collaborators to help design and develop the software. If interested, please open an issue to get in touch or introduce yourself in the project's Discord chat server at `https://di-s-co-rd.gg/p2-a-Y-jM3` (remove the dashes).
## Design
Interactive viewport *(work-in-progress design mockup)*:
![Interactive viewport](https://files.keavon.com/-/EmotionalShoddyTurnstone/capture.png)
Node editor *(work-in-progress design mockup)*:
![Node editor](https://files.keavon.com/-/PartialTalkativePooch/capture.png)
## Technology
[Rust](https://www.rust-lang.org/) is the language of choice for a number of compelling reasons. It is low-level and highly efficient which is important because the nondestructive, resolution-agnostic editing approach will already be challenging to render fast enough for real-time, interactive editing. Furthermore, Rust makes multithreading very easy to implement and its safety guarantees will eliminate the inclusion of many bugs and crashes in the software. It is also easy to compile Rust code natively to Windows, macOS, Linux, and even web browsers via WebAssembly, with the possibility of deploying Graphite to mobile devices down the road as well.
[WebGPU](https://gpuweb.github.io/gpuweb) (via Mozilla's [WGPU Rust library](https://wgpu.rs)) is being used as the graphics API because it is modern, portable, and safe. It makes deployment on the web and native platforms easy while ensuring consistent cross-platform behavior. It also offers the ability to use compute shaders to perform many tasks that speed up graphical computations.
The [GUI framework](gui) is being custom-built for the specific needs of Graphite's interface, based on a simple XML format inspired by HTML, CSS, and Vue.js. This is the current focus of development.
Scripting language: this is to-be-decided. JavaScript (via [Deno's V8 Rust library](https://github.com/denoland/rusty_v8)) is one option, [Mun](https://mun-lang.org/) or Lua are other possibilities, and WebAssembly modules or modular compiled Rust modules are other possibilities.
[Pathfinder](https://github.com/servo/pathfinder) is a Rust library that will be used for vector graphics rendering.
# What is Graphite?
Graphite is an open-source, cross-platform digital content creation desktop app for 2D graphics editing, photo processing, vector art, digital painting, illustration, and compositing. Inspired by the open-source success story of Blender in the 3D domain, it aims to bring 2D content creation to new heights with efficient workflows influenced by Photoshop and Illustrator and backed by a powerful node-based, nondestructive approach proven by Houdini and Substance. The user experience of Graphite is of central importance, offering a meticulously-designed UI catering towards an intuitive and efficient artistic process. Users may draw and edit in the traditional interactive (WYSIWYG) viewport or jump in or out of the node graph at any time to tweak previous work and construct powerful procedural image generators that seamlessly sync with the interactive viewport. A core principle of the application is its 100% nondestructive workflow that is resolution-agnostic, meaning that raster-style image editing can be infinitely zoomed and scaled to any arbitrary resolution at a later time because editing is done by recording brush strokes, vector shapes, and other manipulations parametrically. One might use the painting tools on a small laptop display, zoom into specific areas to add detail to finish the artwork, then perhaps try changing the simulated brush style from a blunt pencil to a soft acrylic paintbrush after-the-fact, and finally export the complete drawing at ultra high resolution for printing on a giant poster. On the surface, Graphite is an artistic medium for drawing anything imaginable— under the hood, the node graph in Graphite powers procedural graphics and parametric rendering to produce unique artwork and automated data-driven visualizations. Graphite brings together artistic workflows and empowers your creativity in a free, open-source package that feels familiar but lets you go further.
## Status
*The code has not had many updates in the past half year as I moved my focus towards hammering out more of the product design and user experience. I'm looking forward to working with interested community members to get back to developing the code further throughout 2021, with the goal of establishing a minimum viable product by end-of-year.*
Graphite is in an early stage of development and its vision is highly ambitious. The project is seeking collaborators to help design and develop the software. If interested, please open an issue to get in touch or introduce yourself in the project's Discord chat server at `https://di-s-co-rd.gg/p2-a-Y-jM3` (remove the dashes).
## Design
Interactive viewport *(work-in-progress design mockup)*:
![Interactive viewport](https://files.keavon.com/-/EmotionalShoddyTurnstone/capture.png)
Node editor *(work-in-progress design mockup)*:
![Node editor](https://files.keavon.com/-/PartialTalkativePooch/capture.png)
## Technology
[Rust](https://www.rust-lang.org/) is the language of choice for a number of compelling reasons. It is low-level and highly efficient which is important because the nondestructive, resolution-agnostic editing approach will already be challenging to render fast enough for real-time, interactive editing. Furthermore, Rust makes multithreading very easy to implement and its safety guarantees will eliminate the inclusion of many bugs and crashes in the software. It is also easy to compile Rust code natively to Windows, macOS, Linux, and web browsers via WebAssembly, with the possibility of deploying Graphite to mobile devices down the road as well.
[WebGPU](https://gpuweb.github.io/gpuweb) (via the [WGPU Rust library](https://wgpu.rs)) is being used as the graphics API because it is modern, portable, and safe. It makes deployment on the web and native platforms easy while ensuring consistent cross-platform behavior. It also offers the ability to use compute shaders to perform many tasks that speed up graphical computations.
[Vue.js](https://vuejs.org/) is the web frontend framework initally used for building Graphite's user interface. This means, for the moment, Graphite will only run in a browser using Rust code compiled to [WebAssembly](https://webassembly.org/) (via [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen)). This web-based GUI is intended to be rewritten in a native Rust GUI framework once that ecosystem matures or a developer can write a custom GUI framework suitable to the subset of featured needed by Graphite's user interface. The project was initially trying to write a custom GUI framework throughout 2020, but this was halting progress on higher-priority features.
Extension scripting language: this is to-be-decided. JavaScript (via [Deno's V8 Rust library](https://github.com/denoland/rusty_v8)) is one option, [Mun](https://mun-lang.org/) or Lua are other possibilities, and WebAssembly modules or modular compiled Rust modules are other possibilities.
[Pathfinder](https://github.com/servo/pathfinder) is a Rust library that will be used for vector graphics rendering.
## Running the code
The project is split between a Rust crates in `/packages` and web-based frontend in `/web-frontend` (this will be replaced by a native GUI system in the future in order to compile Graphite for Windows, Mac, and Linux). Currently the Vue.js frontend runs with the Vue CLI but the WASM bindings HTML/JS is built with WebPack (see [issue #29](https://github.com/Keavon/Graphite/issues/29)).
### Running the web frontend
```
cd web-frontend
npm install
npm run serve
```
### Running the WASM binding generator
PREREQUISITE: [Download and install](https://rustwasm.github.io/wasm-pack/) wasm-pack first.
```
cd web-frontend
npm install
npm run webpack-start
```
### Running the Rust code
```
cargo run
```

13
vetur.config.js Normal file
View file

@ -0,0 +1,13 @@
// vetur.config.js
/** @type {import('vls').VeturConfig} */
module.exports = {
// **optional** default: `{}`
// override vscode settings
// Notice: It only affects the settings used by Vetur.
settings: {
"vetur.useWorkspaceDependencies": true,
},
// **optional** default: `[{ root: './' }]`
// support monorepos
projects: ['./web-frontend']
}

View file

@ -0,0 +1,7 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

23
web-frontend/.eslintrc.js Normal file
View file

@ -0,0 +1,23 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"@vue/airbnb",
"@vue/typescript/recommended",
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-tabs": 0,
"max-len": 0,
"linebreak-style": ["error", "unix"],
indent: ["error", "tab"],
quotes: ["error", "double"],
},
};

12919
web-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,14 @@
{
"name": "graphite-web-frontend",
"version": "0.1.0",
"description": "Graphite's web app frontend pathfinder. Planned to be replaced by a Rust native GUI framework in the future.",
"description": "Graphite's web app frontend. Planned to be replaced by a Rust native GUI framework in the future.",
"main": "main.js",
"scripts": {
"build": "webpack",
"start": "webpack serve"
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"webpack-build": "webpack",
"webpack-start": "webpack serve"
},
"repository": {
"type": "git",
@ -15,10 +18,40 @@
"license": "Apache-2.0",
"homepage": "https://www.graphite.design",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-typescript": "^5.0.2",
"@wasm-tool/wasm-pack-plugin": "^1.3.3",
"html-webpack-plugin": "^5.1.0",
"webpack": "^5.21.2",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-vue": "^7.0.0-0",
"html-webpack-plugin": "^4.5.1",
"lint-staged": "^9.5.0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"vue-svg-loader": "^0.16.0",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.45.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"vue-cli-service lint",
"git add"
]
},
"dependencies": {
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<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">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<body>
<noscript>JavaScript is required</noscript>
<div id="app"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

28
web-frontend/src/App.vue Normal file
View file

@ -0,0 +1,28 @@
<template>
<MainWindow />
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import MainWindow from "./components/window/MainWindow.vue";
@Options({
components: {
MainWindow,
},
})
export default class App extends Vue {}
</script>
<style lang="scss">
html, body, #app {
margin: 0;
height: 100%;
font-family: "Segoe UI", Arial, sans-serif;
font-size: 14px;
line-height: 1;
color: #ddd;
background: #222;
user-select: none;
}
</style>

View file

@ -0,0 +1,60 @@
<template>
<div class="status-area footer-third">
<div>
<span>File: 1.8 MB | Memory: 137 MB | Scratch: 0.7/12.3 GB</span>
</div>
</div>
<div class="hint-area footer-third">
<InputHint :inputMouse="'LMBDrag'">Box Select</InputHint>
<InputHint :inputKeys="['G']">Grab Selection</InputHint>
<InputHint :inputKeys="['R']">Rotate Selection</InputHint>
<InputHint :inputKeys="['S']">Scale Selection</InputHint>
</div>
<div class="version-area footer-third">
<div>
<span>Graphite 0.1.0</span>
</div>
</div>
</template>
<style lang="scss">
.footer-third {
display: flex;
flex: 1 1 100%;
&:nth-child(1) {
justify-content: flex-start;
}
&:nth-child(2) {
justify-content: center;
}
&:nth-child(3) {
justify-content: flex-end;
}
&.status-area div, &.version-area {
margin: 0 8px;
display: flex;
align-items: center;
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue";
import InputHint from "./InputHint.vue";
@Options({
components: {
LayoutRow,
LayoutCol,
InputHint,
},
props: {},
})
export default class FooterBar extends Vue {}
</script>

View file

@ -0,0 +1,144 @@
<template>
<div class="input-hint">
<span class="input-key" v-for="inputKey in inputKeys" :key="inputKey">
{{inputKey}}
</span>
<span class="input-mouse" v-if="inputMouse">
<svg width="16" height="16" viewBox="0 0 16 16" v-html="getMouseIconInnerSVG"></svg>
</span>
<span class="hint-text">
<slot></slot>
</span>
</div>
</template>
<style lang="scss">
.input-hint {
height: 100%;
margin: 0 8px;
display: flex;
align-items: center;
.input-key, .input-mouse {
margin-right: 4px;
}
.input-key {
font-family: "Consolas", monospace;
font-weight: bold;
text-align: center;
color: #000;
background: #fff;
border: 2px;
border-style: solid;
border-color: #999;
border-radius: 4px;
width: 14px;
height: 14px;
line-height: 14px;
}
.input-mouse {
font-size: 0;
.primary {
fill: #fff;
}
.secondary {
fill: #888;
}
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
export enum MouseInputInteraction {
"None" = "None",
"LMB" = "LMB",
"RMB" = "RMB",
"MMB" = "MMB",
"ScrollUp" = "ScrollUp",
"ScrollDown" = "ScrollDown",
"Drag" = "Drag",
"LMBDrag" = "LMBDrag",
"RMBDrag" = "RMBDrag",
"MMBDrag" = "MMBDrag",
}
@Options({
components: {},
props: {
inputKeys: { type: Array, default: [] },
inputMouse: { type: String },
},
computed: {
getMouseIconInnerSVG() {
switch (this.inputMouse) {
case MouseInputInteraction.None: return `
<path style="fill:#888888;" d="M9,7c0,0.55-0.45,1-1,1l0,0C7.45,8,7,7.55,7,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
<path style="fill:#888888;" d="M10,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H10 M10,1H6
C4.35,1,3,2.35,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1L10,1z" />`;
case MouseInputInteraction.LMB: return `
<path style="fill:#FFFFFF;" d="M8,1H6C4.35,1,3,2.35,3,4v4h5V1z" />
<path style="fill:#888888;" d="M10,1H9v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H3v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4
C13,2.35,11.65,1,10,1z" />`;
case MouseInputInteraction.RMB: return `
<path class="secondary" d="M8,1h2c1.65,0,3,1.35,3,3v4H8V1z" />
<path class="primary" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4
C3,2.35,4.35,1,6,1z" />`;
case MouseInputInteraction.MMB: return `
<path style="fill:#FFFFFF;" d="M9,7c0,0.55-0.45,1-1,1l0,0C7.45,8,7,7.55,7,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
<path style="fill:#888888;" d="M10,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H10 M10,1H6
C4.35,1,3,2.35,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1L10,1z" />`;
case MouseInputInteraction.ScrollUp: return `
<polygon style="fill:#FFFFFF;" points="10.5,4 8,2 5.5,4 5.5,2 8,0 10.5,2 " />
<polygon style="fill:#FFFFFF;" points="10.5,8 8,6 5.5,8 5.5,6 8,4 10.5,6 " />
<path style="fill:#888888;" d="M11.5,1.42v1.28C11.8,3.06,12,3.5,12,4v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-0.5,0.2-0.94,0.5-1.29
V1.42C3.61,1.94,3,2.9,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.9,12.39,1.94,11.5,1.42z" />`;
case MouseInputInteraction.ScrollDown: return `
<polygon style="fill:#FFFFFF;" points="5.5,4 8,6 10.5,4 10.5,6 8,8 5.5,6 " />
<polygon style="fill:#FFFFFF;" points="5.5,0 8,2 10.5,0 10.5,2 8,4 5.5,2 " />
<path style="fill:#888888;" d="M11.5,1.42v1.28C11.8,3.06,12,3.5,12,4v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-0.5,0.2-0.94,0.5-1.29
V1.42C3.61,1.94,3,2.9,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.9,12.39,1.94,11.5,1.42z" />`;
case MouseInputInteraction.Drag: return `
<path style="fill:#888888;" d="M8,7c0,0.55-0.45,1-1,1l0,0C6.45,8,6,7.55,6,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
<path style="fill:#888888;" d="M9,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H9 M9,1H5C3.35,1,2,2.35,2,4
v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C12,2.35,10.65,1,9,1L9,1z" />`;
case MouseInputInteraction.LMBDrag: return `
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
<path style="fill:#FFFFFF;" d="M7,1H5C3.35,1,2,2.35,2,4v4h5V1z" />
<path style="fill:#888888;" d="M9,1H8v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H2v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4
C12,2.35,10.65,1,9,1z" />`;
case MouseInputInteraction.RMBDrag: return `
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
<path style="fill:#FFFFFF;" d="M7,1h2c1.65,0,3,1.35,3,3v4H7V1z" />
<path style="fill:#888888;" d="M5,1h1v1H5C3.9,2,3,2.9,3,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4
C2,2.35,3.35,1,5,1z" />`;
case MouseInputInteraction.MMBDrag: return `
<path style="fill:#FFFFFF;" d="M8,7c0,0.55-0.45,1-1,1l0,0C6.45,8,6,7.55,6,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
<path style="fill:#888888;" d="M9,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H9 M9,1H5C3.35,1,2,2.35,2,4
v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C12,2.35,10.65,1,9,1L9,1z" />`;
default: return "";
}
},
},
})
export default class InputHint extends Vue {}
</script>

View file

@ -0,0 +1,55 @@
<template>
<div class="entry">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M5.5,10.2c0.1,0.2,0,0.3-0.1,0.4c-0.2,0.1-0.3,0-0.4-0.1c0,0,0,0,0,0L2.7,6.7c-0.1-0.2,0-0.3,0.1-0.4c0.2-0.1,0.3,0,0.4,0.1
L5.5,10.2z M14.9,14.9L14.9,14.9C14.9,14.9,14.9,15,14.9,14.9C14.8,15,14.8,15,14.9,14.9L14.9,14.9c-0.5,0.2-1,0.4-1.5,0.5
c-0.5,0.1-1.1,0.2-1.6,0.2c-0.5,0.1-1.1,0.1-1.6,0c-0.5,0-1.1-0.1-1.6-0.1l-1.6-0.1c-1.7,0-3.9-0.1-4.4-0.1s-1.1-0.2-1.1-0.3
c0-0.1,0.3-0.2,0.6-0.2c0.5-0.1,1.1-0.2,1.7-0.3c0.6-0.1,2-0.2,4.1-0.2c0.8,0,1.8,0,1.8,0c0.6,0,1,0,1.9,0c0.7,0,1.1,0,1.2,0
c0.3,0,0.6,0.1,0.9,0.2l-2.6-1.9c-0.2,0.1-0.4,0.2-0.7,0.2H4.5c-0.5,0-0.9-0.2-1.1-0.6l-2.9-5c-0.2-0.4-0.2-0.9,0-1.3l2.9-5
C3.6,0.5,4,0.3,4.5,0.3h5.8c0.5,0,0.9,0.2,1.1,0.6l2.9,5l0,0l0,0c0,0.1,0.1,0.2,0.1,0.3c0,0,0,0.1,0,0.1c0,0.1,0,0.1,0,0.2
c0,0,0.7,7.2,0.7,7.8v0C15.2,14.6,15.1,14.8,14.9,14.9L14.9,14.9z M4.3,1.9L4.2,2l3.6,6.2c0.4-0.1,0.9-0.2,1.4-0.2l0.9-1.3l1.6-0.2
c0.2-0.4,0.5-0.8,0.8-1.1l-2.1-3.7c-0.1-0.2-0.3-0.3-0.5-0.3H4.8C4.6,1.6,4.4,1.7,4.3,1.9z M9.4,11.6l-2-1.4C7.4,10.1,7.3,10,7.3,10
l0,0L3.5,3.3l-1.7,3c-0.1,0.2-0.1,0.4,0,0.6l2.6,4.5c0.1,0.2,0.3,0.3,0.5,0.3L9.4,11.6z M13.5,11.3l-0.5-4.6c-0.3,0.3-0.5,0.7-0.7,1
l-1.6,0.2L9.8,9.2c-0.4,0-0.8,0-1.3,0.1l3.7,2.7C12.5,11.6,13,11.3,13.5,11.3L13.5,11.3z" />
</svg>
</div>
<div class="entry"><span>File</span></div>
<div class="entry"><span>Edit</span></div>
<div class="entry"><span>Comp</span></div>
<div class="entry"><span>View</span></div>
<div class="entry"><span>Help</span></div>
</template>
<style lang="scss">
.entry {
display: flex;
align-items: center;
padding: 0 8px;
svg {
fill: #ddd;
}
&:hover {
background: #555;
svg {
fill: #fff;
}
span {
color: #fff;
}
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
props: {},
})
export default class FileMenu extends Vue {}
</script>

View file

@ -0,0 +1,51 @@
<template>
<div class="header-third">
<FileMenu />
</div>
<div class="header-third">
<WindowTitle />
</div>
<div class="header-third">
<WindowButtons :maximized="true" />
</div>
</template>
<style lang="scss">
.header-third {
display: flex;
flex: 1 1 100%;
&:nth-child(1) {
justify-content: flex-start;
}
&:nth-child(2) {
justify-content: center;
}
&:nth-child(3) {
justify-content: flex-end;
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue";
import FileMenu from "./FileMenu.vue";
import WindowTitle from "./WindowTitle.vue";
import WindowButtons from "./WindowButtons.vue";
@Options({
components: {
LayoutRow,
LayoutCol,
FileMenu,
WindowTitle,
WindowButtons,
},
props: {},
})
export default class HeaderBar extends Vue {}
</script>

View file

@ -0,0 +1,54 @@
<template>
<div class="button minimize" title="Minimize">
<svg width="10" height="10" viewBox="0 0 10 10">
<rect y="4" width="10" height="1" />
</svg>
</div>
<div class="button maximize" title="Maximize" v-if="maximized === false">
<svg width="10" height="10" viewBox="0 0 10 10">
<path d="M10,0v10H0V0H10z M9,1H1v8h8V1z" />
</svg>
</div>
<div class="button restore-down" title="Restore Down" v-if="maximized === true">
<svg width="10" height="10" viewBox="0 0 10 10">
<path d="M10,8H8v2H0V2h2V0h8V8z M7,3H1v6h6V3z M9,1H3v1h5v5h1V1z" />
</svg>
</div>
<div class="button close" title="Close">
<svg width="10" height="10" viewBox="0 0 10 10">
<polygon points="10,0.7 9.3,0 5,4.3 0.7,0 0,0.7 4.3,5 0,9.3 0.7,10 5,5.7 9.3,10 10,9.3 5.7,5" />
</svg>
</div>
</template>
<style lang="scss">
.button {
display: flex;
align-items: center;
padding: 0 20px;
svg {
fill: #ddd;
}
&:hover {
background: #555;
svg {
fill: #fff;
}
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
props: {
maximized: { type: Boolean, default: false },
},
})
export default class WindowButtons extends Vue {}
</script>

View file

@ -0,0 +1,23 @@
<template>
<div class="window-title">
<span>X-35B.gdd* - Graphite</span>
</div>
</template>
<style lang="scss">
.window-title {
display: flex;
align-items: center;
padding: 0 8px;
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
props: {},
})
export default class WindowTitle extends Vue {}
</script>

View file

@ -0,0 +1,23 @@
<template>
<div :class="['layout-col']">
<slot></slot>
</div>
</template>
<style lang="scss">
.layout-col {
display: flex;
flex-direction: column;
flex-grow: 1;
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
props: {},
})
export default class LayoutCol extends Vue {}
</script>

View file

@ -0,0 +1,23 @@
<template>
<div :class="['layout-row']">
<slot></slot>
</div>
</template>
<style lang="scss">
.layout-row {
display: flex;
flex-direction: row;
flex-grow: 1;
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
props: {},
})
export default class LayoutRow extends Vue {}
</script>

View file

@ -0,0 +1,146 @@
<template>
<div class="panel">
<div class="tab-bar" :class="{ 'constant-widths': tabConstantWidths }">
<div class="tab" :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabLabel">
<span>{{tabLabel}}</span>
<button v-if="tabCloseButtons"></button>
</div>
</div>
<div class="panel-content">
</div>
</div>
</template>
<style lang="scss">
.panel {
background: #111;
border-radius: 8px;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.tab-bar {
flex-direction: row;
height: 28px;
display: flex;
overflow: hidden;
&.constant-widths .tab {
width: 120px;
}
.tab {
height: 100%;
padding: 0 10px;
display: flex;
align-items: center;
position: relative;
&.active {
background: #333;
border-radius: 8px 8px 0 0;
position: relative;
&::before, &::after {
content: "";
width: 16px;
height: 8px;
position: absolute;
bottom: 0;
box-shadow: #333;
}
&::before {
left: -16px;
border-bottom-right-radius: 8px;
box-shadow: 8px 0 0 0 #333;
}
&::after {
right: -16px;
border-bottom-left-radius: 8px;
box-shadow: -8px 0 0 0 #333;
}
}
span {
flex: 1 1 100%;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
// Required because https://stackoverflow.com/a/21611191/775283
height: 100%;
line-height: 28px;
}
button {
flex: 0 0 auto;
outline: none;
border: none;
padding: 0;
width: 16px;
height: 16px;
background: none;
color: #ddd;
font-weight: bold;
font-size: 10px;
border-radius: 2px;
margin-left: 8px;
&:hover {
background: #555;
color: white;
}
}
&:not(.active) + .tab:not(.active) {
margin-left: 1px;
&::before {
content: "";
position: absolute;
left: -1px;
width: 1px;
height: 16px;
background: #444;
}
}
&:last-of-type:not(.active) {
margin-right: 1px;
&::after {
content: "";
position: absolute;
right: -1px;
width: 1px;
height: 16px;
background: #444;
}
}
}
}
.panel-content {
background: #333;
flex-grow: 1;
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
props: {
tabConstantWidths: { type: Boolean, default: false },
tabCloseButtons: { type: Boolean, default: false },
tabLabels: { type: Array, required: true },
tabActiveIndex: { type: Number, required: true },
},
})
export default class DockablePanel extends Vue {}
</script>

View file

@ -0,0 +1,56 @@
<template>
<LayoutRow class="dockable-grid-subdivision">
<LayoutCol class="dockable-grid-subdivision" style="flex-grow: 1597;">
<DockablePanel :tabCloseButtons="true" :tabConstantWidths="true" :tabLabels="['X-35B*', 'Document 2', 'Document 3', 'Document 4', 'Document 5']" :tabActiveIndex="0" />
</LayoutCol>
<LayoutCol class="dockable-grid-resize-gutter"></LayoutCol>
<LayoutCol class="dockable-grid-subdivision" style="flex-grow: 319;">
<LayoutRow class="dockable-grid-subdivision">
<DockablePanel :tabLabels="['Properties', 'Typography', 'Colors']" :tabActiveIndex="0" />
</LayoutRow>
<LayoutRow class="dockable-grid-resize-gutter"></LayoutRow>
<LayoutRow class="dockable-grid-subdivision">
<DockablePanel :tabLabels="['Layers']" :tabActiveIndex="0" />
</LayoutRow>
<LayoutRow class="dockable-grid-resize-gutter"></LayoutRow>
<LayoutRow class="dockable-grid-subdivision" style="flex-grow: 0; height: 0;">
<DockablePanel :tabLabels="['Minimap', 'Brushes', 'Links']" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
</LayoutRow>
</template>
<style lang="scss">
.dockable-grid-subdivision {
min-height: 28px;
}
.dockable-grid-resize-gutter {
flex: 0 0 4px;
&.layout-row {
cursor: ns-resize;
}
&.layout-col {
cursor: ew-resize;
}
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue";
import DockablePanel from "./DockablePanel.vue";
@Options({
components: {
LayoutRow,
LayoutCol,
DockablePanel,
},
props: {},
})
export default class PanelArea extends Vue {}
</script>

View file

@ -0,0 +1,55 @@
<template>
<LayoutCol class="main-window">
<LayoutRow :class="['header-bar']">
<HeaderBar />
</LayoutRow>
<LayoutRow :class="['panel-container']">
<PanelArea />
</LayoutRow>
<LayoutRow :class="['footer-bar']">
<FooterBar />
</LayoutRow>
</LayoutCol>
</template>
<style lang="scss">
.main-window {
height: 100%;
}
.header-bar {
height: 28px;
flex: 0 0 auto;
}
.panel-container {
flex: 1 1 100%;
height: 100%;
}
.footer-bar {
height: 24px;
flex: 0 0 auto;
}
</style>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue";
import HeaderBar from "../header/HeaderBar.vue";
import PanelArea from "../panel-system/PanelArea.vue";
import FooterBar from "../footer/FooterBar.vue";
@Options({
components: {
LayoutRow,
LayoutCol,
HeaderBar,
PanelArea,
FooterBar,
},
props: {},
})
export default class MainWindow extends Vue {}
</script>

4
web-frontend/src/main.ts Normal file
View file

@ -0,0 +1,4 @@
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

6
web-frontend/src/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View file

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View file

@ -1,23 +1,23 @@
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: "./main.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
plugins: [
new HtmlWebpackPlugin({ title: 'Graphite' }),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, "..", "packages", "wasm-bindings"),
outDir: path.resolve(__dirname, "pkg"),
}),
],
mode: "development",
devtool: 'source-map',
experiments: {
syncWebAssembly: true,
},
};
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: "./main.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
plugins: [
new HtmlWebpackPlugin({ title: "Graphite" }),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, "..", "packages", "wasm-bindings"),
outDir: path.resolve(__dirname, "pkg"),
}),
],
mode: "development",
devtool: "source-map",
// experiments: {
// syncWebAssembly: true,
// },
};