mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 07:08:12 +08:00
Add some additional image effect nodes (#869)
* Move the Subpath type to graphene-std * Add the transform subpath node * Delete selected nodes * Inserting node list on right click * Add several bitmap manipulator nodes * Convert add node to use f64 * Add posterize node * Rename names randomly * Fix naming * Exposure node * Fix typo * Adjust exposure node range * Comment out vector nodes * Adjust exposure range again * Posterise as ints * Rename input * Use >= in the to hsl function
This commit is contained in:
+2
-11
@@ -40,27 +40,18 @@
|
||||
--color-f-white-rgb: 255, 255, 255;
|
||||
|
||||
--color-data-general: #c5c5c5;
|
||||
--color-data-general-rgb: 197, 197, 197;
|
||||
--color-data-general-dim: #767676;
|
||||
--color-data-general-dim-rgb: 118, 118, 118;
|
||||
--color-data-vector: #65bbe5;
|
||||
--color-data-vector-rgb: 101, 187, 229;
|
||||
--color-data-vector-dim: #4b778c;
|
||||
--color-data-vector-dim-rgb: 75, 119, 140;
|
||||
--color-data-raster: #e4bb72;
|
||||
--color-data-raster-rgb: 228, 187, 114;
|
||||
--color-data-raster-dim: #8b7752;
|
||||
--color-data-raster-dim-rgb: 139, 119, 82;
|
||||
--color-data-mask: #8d85c7;
|
||||
--color-data-mask-rgb: 141, 133, 199;
|
||||
--color-data-number: #d6536e;
|
||||
--color-data-number-rgb: 214, 83, 110;
|
||||
--color-data-number-dim: #803242;
|
||||
--color-data-number-dim-rgb: 128, 50, 66;
|
||||
--color-data-vec2: #cc00ff;
|
||||
--color-data-vec2-dim: #71008d;
|
||||
--color-data-color: #70a898;
|
||||
--color-data-color-rgb: 112, 168, 152;
|
||||
--color-data-color-dim: #43645b;
|
||||
--color-data-color-dim-rgb: 67, 100, 91;
|
||||
|
||||
--color-none: white;
|
||||
--color-none-repeat: no-repeat;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<LayoutCol class="node-graph">
|
||||
<LayoutRow class="options-bar"></LayoutRow>
|
||||
<div class="node-list">
|
||||
<LayoutRow>Nodes:</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextButton v-for="nodeType in nodeTypes" v-bind:key="String(nodeType)" :label="nodeType.name + ' Node'" :action="() => createNode(nodeType.name)"></TextButton>
|
||||
</LayoutRow>
|
||||
</div>
|
||||
<LayoutRow
|
||||
class="graph"
|
||||
ref="graph"
|
||||
@@ -21,6 +15,14 @@
|
||||
'--dot-radius': `${dotRadius}px`,
|
||||
}"
|
||||
>
|
||||
<LayoutCol class="node-list" v-if="nodeListLocation" :style="{ marginLeft: `${nodeListX}px`, marginTop: `${nodeListY}px` }">
|
||||
<TextInput placeholder="Search Nodes..." :value="searchTerm" @update:value="(val) => (searchTerm = val)" v-focus />
|
||||
<LayoutCol v-for="nodeCategory in nodeCategories" :key="nodeCategory[0]">
|
||||
<TextLabel>{{ nodeCategory[0] }}</TextLabel>
|
||||
<TextButton v-for="nodeType in nodeCategory[1]" v-bind:key="String(nodeType)" :label="nodeType.name + ' Node'" :action="() => createNode(nodeType.name)" />
|
||||
</LayoutCol>
|
||||
<TextLabel v-if="nodeCategories.length === 0">No search results :(</TextLabel>
|
||||
</LayoutCol>
|
||||
<div
|
||||
class="nodes"
|
||||
ref="nodesContainer"
|
||||
@@ -37,8 +39,6 @@
|
||||
:style="{
|
||||
'--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0),
|
||||
'--offset-top': (node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0),
|
||||
'--data-color': 'var(--color-data-raster)',
|
||||
'--data-color-dim': 'var(--color-data-raster-dim)',
|
||||
}"
|
||||
:data-node="node.id"
|
||||
>
|
||||
@@ -111,10 +111,9 @@
|
||||
.node-list {
|
||||
width: max-content;
|
||||
position: fixed;
|
||||
padding: 20px;
|
||||
margin: 40px 10px;
|
||||
padding: 5px;
|
||||
z-index: 3;
|
||||
background-color: var(--color-4-dimgray);
|
||||
background-color: var(--color-3-darkgray);
|
||||
}
|
||||
|
||||
.options-bar {
|
||||
@@ -181,7 +180,7 @@
|
||||
border-radius: 4px;
|
||||
background: var(--color-4-dimgray);
|
||||
left: calc((var(--offset-left) + 0.5) * 24px);
|
||||
top: calc((var(--offset-top) + 0.5) * 24px);
|
||||
top: calc((var(--offset-top) - 0.5) * 24px);
|
||||
|
||||
&.selected {
|
||||
border: 1px solid var(--color-e-nearwhite);
|
||||
@@ -209,6 +208,7 @@
|
||||
|
||||
.arguments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
@@ -297,6 +297,7 @@ import type { IconName } from "@/utility-functions/icons";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
@@ -316,6 +317,8 @@ export default defineComponent({
|
||||
linkInProgressFromConnector: undefined as HTMLDivElement | undefined,
|
||||
linkInProgressToConnector: undefined as HTMLDivElement | DOMRect | undefined,
|
||||
nodeLinkPaths: [] as [string, string][],
|
||||
searchTerm: "",
|
||||
nodeListLocation: undefined as { x: number; y: number } | undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -335,8 +338,24 @@ export default defineComponent({
|
||||
nodes() {
|
||||
return this.nodeGraph.state.nodes;
|
||||
},
|
||||
nodeTypes() {
|
||||
return this.nodeGraph.state.nodeTypes;
|
||||
nodeCategories() {
|
||||
const categories = new Map();
|
||||
this.nodeGraph.state.nodeTypes.forEach((node) => {
|
||||
if (this.searchTerm.length && !node.name.toLowerCase().includes(this.searchTerm.toLowerCase()) && !node.category.toLowerCase().includes(this.searchTerm.toLowerCase())) return;
|
||||
|
||||
const category = categories.get(node.category);
|
||||
if (category) category.push(node);
|
||||
else categories.set(node.category, [node]);
|
||||
});
|
||||
|
||||
const result = Array.from(categories);
|
||||
return result;
|
||||
},
|
||||
nodeListX() {
|
||||
return ((this.nodeListLocation?.x || 0) * GRID_SIZE + this.transform.x) * this.transform.scale;
|
||||
},
|
||||
nodeListY() {
|
||||
return ((this.nodeListLocation?.y || 0) * GRID_SIZE + this.transform.y) * this.transform.scale;
|
||||
},
|
||||
linkPathInProgress(): [string, string] | undefined {
|
||||
if (this.linkInProgressFromConnector && this.linkInProgressToConnector) {
|
||||
@@ -461,8 +480,19 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
pointerDown(e: PointerEvent) {
|
||||
if (e.button === 2) {
|
||||
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
const graph = graphDiv?.getBoundingClientRect() || new DOMRect();
|
||||
this.nodeListLocation = {
|
||||
x: Math.round(((e.clientX - graph.x) / this.transform.scale - this.transform.x) / GRID_SIZE),
|
||||
y: Math.round(((e.clientY - graph.y) / this.transform.scale - this.transform.y) / GRID_SIZE),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
|
||||
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||
const nodeList = (e.target as HTMLElement).closest(".node-list") as HTMLElement | undefined;
|
||||
|
||||
if (port) {
|
||||
const isOutput = Boolean(port.getAttribute("data-port") === "output");
|
||||
@@ -488,7 +518,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
|
||||
} else {
|
||||
} else if (!nodeList) {
|
||||
this.selected = [];
|
||||
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
|
||||
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
@@ -560,7 +590,10 @@ export default defineComponent({
|
||||
this.linkInProgressToConnector = undefined;
|
||||
},
|
||||
createNode(nodeType: string): void {
|
||||
this.editor.instance.createNode(nodeType);
|
||||
if (!this.nodeListLocation) return;
|
||||
|
||||
this.editor.instance.createNode(nodeType, this.nodeListLocation.x, this.nodeListLocation.y);
|
||||
this.nodeListLocation = undefined;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -578,6 +611,7 @@ export default defineComponent({
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
TextButton,
|
||||
TextInput,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="parameter-expose-button">
|
||||
<button :class="{ exposed }" :style="{ '--data-type-color': dataTypeColor }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
|
||||
<button :class="{ exposed }" :style="{ '--data-type-color': `var(--color-data-${dataType})` }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
@@ -53,21 +53,6 @@ export default defineComponent({
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||
},
|
||||
computed: {
|
||||
dataTypeColor(): string {
|
||||
// TODO: Move this function somewhere where it can be reused by other components
|
||||
const colorsMap = {
|
||||
general: "var(--color-data-general)",
|
||||
vector: "var(--color-data-vector)",
|
||||
raster: "var(--color-data-raster)",
|
||||
mask: "var(--color-data-mask)",
|
||||
number: "var(--color-data-number)",
|
||||
color: "var(--color-data-color)",
|
||||
} as const;
|
||||
|
||||
return colorsMap[this.dataType as keyof typeof colorsMap] || colorsMap.general;
|
||||
},
|
||||
},
|
||||
components: { LayoutRow },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
@@ -148,6 +149,7 @@ export default defineComponent({
|
||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
placeholder: { type: String as PropType<string>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
:placeholder="placeholder"
|
||||
:style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }"
|
||||
:sharpRightCorners="sharpRightCorners"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@@ -41,6 +42,7 @@ export default defineComponent({
|
||||
// Label
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
placeholder: { type: String as PropType<string>, required: false },
|
||||
|
||||
// Disabled
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
+16
-1
@@ -17,5 +17,20 @@ import App from "@/App.vue";
|
||||
await initWasm();
|
||||
|
||||
// Initialize the Vue application
|
||||
createApp(App).mount("#app");
|
||||
createApp(App)
|
||||
.directive("focus", {
|
||||
// When the bound element is inserted into the DOM
|
||||
mounted(el) {
|
||||
let focus = el;
|
||||
|
||||
// Find actual relevant child
|
||||
while (focus.children.length) focus = focus.children[0];
|
||||
|
||||
// Random timeout needed?
|
||||
setTimeout(() => {
|
||||
focus.focus(); // Focus the element
|
||||
}, 0);
|
||||
},
|
||||
})
|
||||
.mount("#app");
|
||||
};
|
||||
|
||||
@@ -102,6 +102,8 @@ export class FrontendNodeLink {
|
||||
|
||||
export class FrontendNodeType {
|
||||
readonly name!: string;
|
||||
|
||||
readonly category!: string;
|
||||
}
|
||||
|
||||
export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||
|
||||
Reference in New Issue
Block a user