diff --git a/README.md b/README.md index c7a04da..a653f39 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Another Flow-based Node graph Library. +## About + +Node Flow is a javascript library that enables developers to build node based tools similar to Unreal Blueprints or Blender Nodes. + + ## Development ```bash diff --git a/index.html b/index.html index 5630ce1..b7219a5 100644 --- a/index.html +++ b/index.html @@ -43,35 +43,7 @@ NodeFlowTheme.FontFamily = "Source Code Pro"; - const node1 = new FlowNode({ title: "Test Node 1", position: { x: 0, y: 100 } }); - node1.addOutput({ name: "test out 1", type: "string" }) - node1.addOutput({ name: "test out 2", type: "int" }) - node1.addOutput({ name: "A super duper long name!!!", type: "bool" }) - node1.addWidget(new ColorWidget({ value: "#000000" })); - node1.addWidget(new NumberWidget({ value: 1.2345 })); - node1.addWidget(new StringWidget({ value: "woo!" })); - node1.addWidget(new StringWidget({ value: "A super duper long text input" })); - node1.addWidget(new ButtonWidget({ - text: "Click Me!", - callback: () => { - alert("Node button clicked!") - } - })); - node1.addWidget(new ToggleWidget({ - text: "Example", - })); - node1.addWidget(new SliderWidget({ - min: 0, - max: 10, - value: 7.5 - })) - - const node2 = new FlowNode({ - title: "Test Node 2 With a super long Title", - position: { x: 300, y: 0 }, - }); - node2.addInput({ name: "test input 1", type: "string" }) - node2.addOutput({ name: "test out 1", type: "int" }) + const node3 = new FlowNode({ title: "Node 3", @@ -200,26 +172,131 @@ * H1, H2, and H3 * Bold and Italic * Unordered Lists +* Code blocks +`, + position: { + x: 2300, + y: 20 + }, + locked: true, + }, + { + position: { + x: 20, + y: 20 + }, + locked: true, + text: ` +# Node Flow + +Node Flow is a javascript library that enables developers to build node based tools similar to Unreal Blueprints or Blender Nodes. -More will come, so feel free to open an issue for desired features! (Please search and check to make sure one hasn't already been opened up in the past) - `, +## Example + +Scattered across this graph contains code snippets on how to accomplish different things within the library. + +\`\`\` +// Create a canvas to render our graph to +var canvas = document.createElement("canvas"); + +// Create our Node Flow Graph +var graph = new NodeFlowGraph(canvas) +\`\`\` +` + }, + { position: { - x: 300, - y: 180 + x: 800, + y: 20 }, locked: true, + text: ` +## Inputs and Outputs + +Create a Add node that takes two numbers and outputs a single number + +\`\`\` +var node = new FlowNode({ + title: "Add", + inputs: [ + { name: "a", type: "float32" }, + { name: "b", type: "float32" } + ], + outputs: [ + { name: "sum", type: "float32" } + ], +}); +graph.addNode(node); +\`\`\` + +You can also add additional inputs and outputs to the node after it's been created + +\`\`\` +node.addInput({ name: "c", type: "float32" }) +node.addOutput({ name: "sum", type: "float32" }) +\`\`\` +` } ] }); - graph.addNode(node1); - graph.addNode(node2); - graph.addNode(node3); - graph.connectNodes(node1, 0, node2, 0) - graph.connectNodes(node2, 0, node3, 0) - // graph.organize(); - + var sumNode = new FlowNode({ + position: { + x: 850, + y: 600 + }, + title: "Add", + inputs: [ + { name: "a", type: "float32" }, + { name: "b", type: "float32" } + ], + outputs: [ + { name: "sum", type: "float32" } + ], + }); + + + var aNode = new FlowNode({ + position: { + x: 550, + y: 500 + }, + title: "Number", + outputs: [ + { name: "value", type: "float32" } + ], + widgets: [ + { + type: "number", + } + ] + }); + + var bNode = new FlowNode({ + position: { + x: 550, + y: 650 + }, + title: "Number", + outputs: [ + { name: "value", type: "float32" } + ], + widgets: [ + { + type: "number", + } + ] + }) + + graph.addNode(sumNode) + graph.addNode(aNode) + graph.addNode(bNode) + + graph.connectNodes(aNode, 0, sumNode, 0) + graph.connectNodes(bNode, 0, sumNode, 1) + + \ No newline at end of file diff --git a/src/markdown/entry.ts b/src/markdown/entry.ts index 04c97ed..24b8e5a 100644 --- a/src/markdown/entry.ts +++ b/src/markdown/entry.ts @@ -7,6 +7,104 @@ export interface MarkdownEntry { render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number } +export class CodeBlockEntry { + + #text: Text + + #calculatedPositions: List + + #calculatedEntries: List + + #calculatedForWidth: number; + + constructor(text: Text) { + this.#text = text; + this.#calculatedForWidth = -1 + + this.#calculatedEntries = new List(); + this.#calculatedPositions = new List(); + + document.fonts.addEventListener("loadingdone", (event) => { + this.#calculatedForWidth = -1 + }); + } + + #calculateLayout(ctx: CanvasRenderingContext2D, maxWidth: number): void { + if (this.#calculatedForWidth === maxWidth) { + return; + } + + let adjustedWith = maxWidth; + adjustedWith -= Theme.Note.CodeBlock.Padding * 2; + + this.#calculatedEntries.Clear(); + this.#calculatedPositions.Clear(); + + let curHeight = 0; + const lineInc = this.#text.style().getSize() + Theme.Note.LineSpacing; + + let entries = this.#text.split("\n") + + for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) { + const entry = entries[entryIndex]; + + let lines = entry.breakIntoLines(ctx, adjustedWith); + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + + this.#calculatedEntries.Push(lines[lineIndex]) + this.#calculatedPositions.Push({ + x: 0, + y: curHeight, + }) + + curHeight += lineInc; + } + } + + this.#calculatedForWidth = maxWidth; + } + + render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number { + this.#calculateLayout(ctx, maxWidth); + + let padding = Theme.Note.CodeBlock.Padding * scale; + + let max = 0; + for (let i = 0; i < this.#calculatedEntries.Count(); i++) { + const pos = this.#calculatedPositions.At(i); + max = Math.max(max, pos.y * scale) + } + + const y = position.y + max + (scale * 5) + ctx.fillStyle = Theme.Note.CodeBlock.BackgroundColor; + ctx.beginPath(); + ctx.roundRect( + position.x, + position.y, + maxWidth * scale, + max + (padding * 2), + Theme.Note.CodeBlock.BorderRadius * scale + ); + ctx.fill(); + + + for (let i = 0; i < this.#calculatedEntries.Count(); i++) { + const entry = this.#calculatedEntries.At(i); + const pos = this.#calculatedPositions.At(i); + + entry.render(ctx, scale, { + x: (pos.x * scale) + position.x + padding, + y: (pos.y * scale) + position.y + padding + }); + + max = Math.max(max, pos.y * scale) + } + + + return max + (padding * 2); + } +} + export class UnorderedListMarkdownEntry { #entries: Array; @@ -44,6 +142,8 @@ export class BasicMarkdownEntry { #underline: boolean; + #background: boolean; + #entries: Array #calculatedPositions: List @@ -52,9 +152,10 @@ export class BasicMarkdownEntry { #calculatedForWidth: number; - constructor(lines: Array, underline: boolean) { + constructor(lines: Array, underline: boolean, background: boolean) { this.#entries = lines; this.#underline = underline; + this.#background = background; this.#calculatedForWidth = -1 this.#calculatedEntries = new List(); @@ -69,6 +170,12 @@ export class BasicMarkdownEntry { if (this.#calculatedForWidth === maxWidth) { return; } + + let adjustedWith = maxWidth; + if (this.#background) { + adjustedWith -= Theme.Note.CodeBlock.Padding * 2; + } + this.#calculatedEntries.Clear(); this.#calculatedPositions.Clear(); @@ -79,11 +186,10 @@ export class BasicMarkdownEntry { const currentLineText = new List(); const currentLineWidths = new List(); - for (let entryIndex = 0; entryIndex < this.#entries.length; entryIndex++) { const entry = this.#entries[entryIndex]; - let lines = entry.splitAtWidth(ctx, maxWidth - curPosition.x); + let lines = entry.splitAtWidth(ctx, adjustedWith - curPosition.x); let i = 0; while (lines.length > 1 && i < 100) { i++ @@ -132,7 +238,7 @@ export class BasicMarkdownEntry { currentLineHeight = 0; curPosition.x = 0; - lines = lines[1].splitAtWidth(ctx, maxWidth); + lines = lines[1].splitAtWidth(ctx, adjustedWith); } if (i === 100) { console.log(lines) @@ -161,14 +267,41 @@ export class BasicMarkdownEntry { render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number { this.#calculateLayout(ctx, maxWidth); + let padding = 0; + if (this.#background) { + padding += Theme.Note.CodeBlock.Padding * scale; + } + + + if (this.#background) { + let max = 0; + for (let i = 0; i < this.#calculatedEntries.Count(); i++) { + const pos = this.#calculatedPositions.At(i); + max = Math.max(max, pos.y * scale) + } + + const y = position.y + max + (scale * 5) + ctx.fillStyle = Theme.Note.CodeBlock.BackgroundColor; + ctx.beginPath(); + ctx.roundRect( + position.x, + position.y, + maxWidth * scale, + max + (padding * 2), + Theme.Note.CodeBlock.BorderRadius * scale + ); + ctx.fill(); + } + + let max = 0; for (let i = 0; i < this.#calculatedEntries.Count(); i++) { const entry = this.#calculatedEntries.At(i); const pos = this.#calculatedPositions.At(i); entry.render(ctx, scale, { - x: (pos.x * scale) + position.x, - y: (pos.y * scale) + position.y + x: (pos.x * scale) + position.x + padding, + y: (pos.y * scale) + position.y + padding }); max = Math.max(max, pos.y * scale) @@ -185,6 +318,6 @@ export class BasicMarkdownEntry { ctx.stroke(); } - return max; + return max + (padding * 2); } } diff --git a/src/markdown/lexicalParser.ts b/src/markdown/lexicalParser.ts index 2a72c26..0a3ee0f 100644 --- a/src/markdown/lexicalParser.ts +++ b/src/markdown/lexicalParser.ts @@ -37,8 +37,11 @@ export class MarkdownLexicalParser { return this.#body.charAt(this.#index); } + #lastToken = 0; + #addToken(token: MarkdownTokenType, lexeme: string): void { - this.#tokens.push(new MarkdownToken(token, lexeme)); + this.#tokens.push(new MarkdownToken(token, lexeme, this.#lastToken, this.#index)); + this.#lastToken = this.#index; } #h2(): void { @@ -91,13 +94,14 @@ export class MarkdownLexicalParser { } // End of the line! - if (char === "\n" || char === "*") { + if (char === "\n" || char === "*" || char === "`") { if (started != -1) { this.#addToken(MarkdownTokenType.Text, this.#body.substring(started, this.#index)) } return; } + if (started === -1) { started = this.#index; } @@ -131,6 +135,9 @@ export class MarkdownLexicalParser { } else if (char === "*") { this.#addToken(MarkdownTokenType.Star, "*"); this.#inc(); + } else if (char === "`") { + this.#addToken(MarkdownTokenType.BackTick, "`"); + this.#inc(); } else { this.#text(); } diff --git a/src/markdown/markdown.ts b/src/markdown/markdown.ts index b1dcc54..9d80bad 100644 --- a/src/markdown/markdown.ts +++ b/src/markdown/markdown.ts @@ -7,7 +7,7 @@ export function BuildMarkdown(data: string): Array { lexicalParser.parse(); // console.log(lexicalParser.tokens()); - const syntaxParser = new MarkdownSyntaxParser(lexicalParser.tokens()); + const syntaxParser = new MarkdownSyntaxParser(data, lexicalParser.tokens()); const contents = syntaxParser.parse() // console.log(contents); return contents; diff --git a/src/markdown/syntaxParser.ts b/src/markdown/syntaxParser.ts index 5841ca8..013323b 100644 --- a/src/markdown/syntaxParser.ts +++ b/src/markdown/syntaxParser.ts @@ -1,7 +1,7 @@ import { FontStyle, FontWeight, TextStyleConfig } from "../styles/text"; import { Theme } from "../theme"; import { Text } from "../types/text"; -import { BasicMarkdownEntry, MarkdownEntry, UnorderedListMarkdownEntry } from "./entry"; +import { BasicMarkdownEntry, CodeBlockEntry, MarkdownEntry, UnorderedListMarkdownEntry } from "./entry"; import { MarkdownToken, MarkdownTokenType } from "./token"; export class MarkdownSyntaxParser { @@ -10,7 +10,10 @@ export class MarkdownSyntaxParser { #index: number; - constructor(tokens: Array) { + #originalText: string; + + constructor(originalText: string, tokens: Array) { + this.#originalText = originalText; this.#index = 0; this.#tokens = tokens; } @@ -41,6 +44,13 @@ export class MarkdownSyntaxParser { return this.#tokens[this.#index + 1]; } + #peakInto(amount: number): MarkdownToken | null { + if (this.#index + amount >= this.#tokens.length - 1) { + return null + } + return this.#tokens[this.#index + amount]; + } + #emphasis(): Array { // Cases to consider @@ -176,7 +186,7 @@ export class MarkdownSyntaxParser { text[i].setWeight(FontWeight.Bold); } - return new BasicMarkdownEntry(text, true); + return new BasicMarkdownEntry(text, true, false); } #h2(): BasicMarkdownEntry { @@ -190,7 +200,7 @@ export class MarkdownSyntaxParser { text[i].setWeight(FontWeight.Bold); } - return new BasicMarkdownEntry(text, true); + return new BasicMarkdownEntry(text, true, false); } #h3(): BasicMarkdownEntry { @@ -204,14 +214,14 @@ export class MarkdownSyntaxParser { text[i].setWeight(FontWeight.Bold); } - return new BasicMarkdownEntry(text, false); + return new BasicMarkdownEntry(text, false, false); } #starLineStart(): MarkdownEntry { if (this.#peak()?.type() !== MarkdownTokenType.Space) { const starEntries = this.#text(); this.#assignStandardStyling(starEntries); - return new BasicMarkdownEntry(starEntries, false); + return new BasicMarkdownEntry(starEntries, false, false); } // We've begun with a space, which means unordered list! @@ -222,7 +232,7 @@ export class MarkdownSyntaxParser { this.#inc(); const starEntries = this.#text(); this.#assignStandardStyling(starEntries); - entries.push(new BasicMarkdownEntry(starEntries, false)); + entries.push(new BasicMarkdownEntry(starEntries, false, false)); // Text reads to the end of the line, which means we're at the new // line character. Move forward 1. @@ -240,6 +250,53 @@ export class MarkdownSyntaxParser { } } + #backtickLineStart(): MarkdownEntry { + + // If we're not a ```, just treat as regular text + if (this.#peak()?.type() !== MarkdownTokenType.BackTick || this.#peakInto(2)?.type() !== MarkdownTokenType.BackTick) { + const starEntries = this.#text(); + this.#assignStandardStyling(starEntries); + return new BasicMarkdownEntry(starEntries, false, false); + } + this.#inc(); + this.#inc(); + + let token = this.#next(); + + // Eat the first new line character + if (token?.type() === MarkdownTokenType.NewLine) { + token = this.#next(); + } + + let start = token?.tokenStart(); + let end = start; + + // Read until we hit another ``` + while (token !== null) { + + if ( + token.type() === MarkdownTokenType.BackTick && + this.#peak()?.type() === MarkdownTokenType.BackTick && + this.#peakInto(2)?.type() === MarkdownTokenType.BackTick + ) { + end = token.tokenEnd() - 1; + break; + } + + token = this.#next(); + } + + this.#inc(); // puts us on the 2nd + this.#inc(); // puts us on the 3rd + this.#inc(); // puts us on the next token + + const codeBlockText = this.#originalText.substring(start as number, end); + const entryText = new Text(codeBlockText); + console.log(codeBlockText) + this.#assignStandardStyling([entryText]) + return new CodeBlockEntry(entryText) + } + parse(): Array { let token = this.#current(); @@ -262,7 +319,7 @@ export class MarkdownSyntaxParser { case MarkdownTokenType.Text: const textEntries = this.#text(); this.#assignStandardStyling(textEntries); - entries.push(new BasicMarkdownEntry(textEntries, false)); + entries.push(new BasicMarkdownEntry(textEntries, false, false)); break; case MarkdownTokenType.Star: @@ -273,6 +330,10 @@ export class MarkdownSyntaxParser { this.#inc(); break; + case MarkdownTokenType.BackTick: + entries.push(this.#backtickLineStart()); + break; + default: this.#inc(); } diff --git a/src/markdown/token.ts b/src/markdown/token.ts index 0d2406e..3aceb47 100644 --- a/src/markdown/token.ts +++ b/src/markdown/token.ts @@ -5,7 +5,8 @@ export enum MarkdownTokenType { H3 = "H3", NewLine = "New Line", Star = "Star", - Space = "Space" + Space = "Space", + BackTick = "`" } export class MarkdownToken { @@ -14,9 +15,15 @@ export class MarkdownToken { #lexeme: string - constructor(type: MarkdownTokenType, lexeme: string) { + #tokenStart: number + + #tokenEnd: number + + constructor(type: MarkdownTokenType, lexeme: string, tokenStart: number, tokenEnd: number) { this.#type = type; this.#lexeme = lexeme; + this.#tokenStart = tokenStart; + this.#tokenEnd = tokenEnd; } type(): MarkdownTokenType { @@ -26,4 +33,11 @@ export class MarkdownToken { lexeme(): string { return this.#lexeme; } + + tokenStart(): number { + return this.#tokenStart; + } + tokenEnd(): number { + return this.#tokenEnd; + } } diff --git a/src/node.ts b/src/node.ts index a19093c..0a8374d 100644 --- a/src/node.ts +++ b/src/node.ts @@ -280,6 +280,7 @@ export class FlowNode { // Add some padding at the end! size.y += this.#elementSpacing + size.x = Math.max(size.x, 150) size.x *= scale; size.y *= scale; diff --git a/src/organize.ts b/src/organize.ts index 51c3ecb..7de7e94 100644 --- a/src/organize.ts +++ b/src/organize.ts @@ -75,6 +75,22 @@ export function Organize(ctx: CanvasRenderingContext2D, graph: NodeFlowGraph): v entries.sort((a, b) => b.length - a.length); + interface Column { + Nodes: Array; + Width: number; + } + + const columns = Array(entries[0].length + 1); + for (let i = 0; i < columns.length; i++) { + columns[i] = { + Nodes: new Array(), + Width: 0 + }; + } + + console.log(relativePosition) + console.log(entries) + for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (claimed[entry.node] === true) { @@ -92,18 +108,44 @@ export function Organize(ctx: CanvasRenderingContext2D, graph: NodeFlowGraph): v if (claimed[p] === true) { continue; } + + const nodeBounds = bounds[p]; + const column = columns[position - entry.min]; + column.Nodes.push(nodes[p]) + column.Width = Math.max(column.Width, nodeBounds.Size.x) + + claimed[p] = true; + } + } + + let allColumnsWidths = 0; + for (let c = 0; c < columns.length; c++) { + allColumnsWidths += columns[c].Width; + } + + const widthSpacing = 100; + const heightSpacing = 100; + + let widthOffset = 0; + for (let c = 0; c < columns.length; c++) { + var column = columns[c]; + let heightOffset = 0; + widthOffset -= widthSpacing + column.Width; + + for (let n = 0; n < column.Nodes.length; n++) { + const node = column.Nodes[n]; + const nodeBounds = bounds[nodeLUT.get(node) as number]; + let pos = { - x: -(position - entry.min ) * 400, - y: i * 300 + x: widthOffset + allColumnsWidths + (columns.length * widthSpacing), + y: heightOffset } + + heightOffset += nodeBounds.Size.y + heightSpacing console.log(pos); - nodes[p].setPosition(pos) - - claimed[p] = true; + node.setPosition(pos); } } - console.log(relativePosition) - console.log(entries); } \ No newline at end of file diff --git a/src/theme.ts b/src/theme.ts index 3515cba..10a4966 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -56,6 +56,11 @@ export const Theme = { }, H3: { FontSize: 16, + }, + CodeBlock: { + BackgroundColor: "#0c2b35", + Padding: 16, + BorderRadius: 4, } } } \ No newline at end of file diff --git a/src/types/text.ts b/src/types/text.ts index dfc57d1..a2e1083 100644 --- a/src/types/text.ts +++ b/src/types/text.ts @@ -51,6 +51,17 @@ export class Text { return results; } + split(char: string): Array { + const entries = this.#value.split(char) + const results = new Array(); + for (let i = 0; i < entries.length; i++) { + const text = new Text(entries[i]) + text.#style = this.#style; + results.push(text) + } + return results; + } + splitAtIndex(index: number): Array { const results = [ new Text(this.#value.substring(0, index)), @@ -116,4 +127,8 @@ export class Text { this.#style.setupStyle(ctx, scale) ctx.fillText(this.#value, position.x, position.y); } + + style(): TextStyle { + return this.#style; + } } \ No newline at end of file