Initial commit: visual programming framework with web components

HTML defines template-based UI for flow chart nodes (start, end, output, decision, datum). CSS provides grid-based canvas and node styling. JS implements custom elements with drag-and-drop, linkable connections, and C code generation.
This commit is contained in:
2026-06-16 12:52:53 +02:00
commit 91f583c51c
3 changed files with 386 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link href="style.css" rel="stylesheet">
<script src="main.js"></script>
</head>
<body onload="main()">
<template id="wc-start">
<wc-start>
START
<button class="hook"></button>
</wc-start>
</template>
<template id="wc-end">
<wc-end>
<button class="hook"></button>
END
</wc-end>
</template>
<template id="wc-output">
<wc-output>
<textarea id="" placeholder="output string"></textarea>
<button class="hook" id="hook-fore">in</button>
<button class="hook" id="hook-next">out</button>
</wc-output>
</template>
<template id="wc-decision">
<wc-decision>
<p>BRANCH</p>
<button class="hook" id="hook-test">test</button>
<button class="hook" id="hook-out1">yes</button>
<button class="hook" id="hook-out0">no</button>
<button class="hook" id="hook-fore">in</button>
</wc-decision>
</template>
<template id="wc-datum">
<wc-datum>
DATUM
<button class="hook"></button>
</wc-datum>
</template>
<h1>The anti-vibecoding framework</h1>
<noscript>bro thinks this works without js &#x1F480;</noscript>
<button onclick="$('#main-program').newDecisionNode()">new decision</button>
<button onclick="$('#main-program').newOutputNode()">new output</button>
<button onclick="$('#main-program').newDataNode()">new data</button>
<button onclick="$('#main-program').generateCode()">Generate code</button>
<textarea id="c-code"></textarea>
<div id="draw-area" onclick="linkedFirst=null">
<svg id="connections" xmlns="http://www.w3.org/2000/svg">
</svg>
<wc-program id="main-program">
</wc-program>
</div>
</body>
</html>
+203
View File
@@ -0,0 +1,203 @@
let $ = document.querySelector.bind(document);
let $$ = document.querySelectorAll.bind(document);
const svgNamespace = 'http://www.w3.org/2000/svg';
let linkageFirst = null
class WCBlock extends HTMLElement {
constructor() {
super();
this.addEventListener("mousedown", this.startDragging);
this.querySelector(".hook").addEventListener("click", this.linkEvent.bind(this));
/* List of connections to and from this */
this.wcForeFlow = Array.of();
this.wcNextFlow = Array.of();
}
startDragging(event) {
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
document.onmousemove = this.dragAround.bind(this);
document.onmouseup = this.stopDragging.bind(this);
}
dragAround(event) {
const {clientX, clientY} = event;
const gridSize = parseInt(window.getComputedStyle(this).getPropertyValue('--grid-size'));
let newTop = (this.offsetTop + event.clientY - this.dragStartY);
let newLeft = (this.offsetLeft + event.clientX - this.dragStartX);
newTop = Math.round(newTop / gridSize) * gridSize;
newLeft = Math.round(newLeft / gridSize) * gridSize;
this.dragStartX = Math.round(event.clientX/gridSize) * gridSize;
this.style.top = newTop +"px";
this.dragStartY = Math.round(event.clientY/gridSize) * gridSize;
this.style.left = newLeft +"px";
this.wcNextFlow.forEach(element => {
drawpath(element);
});
this.wcForeFlow.forEach(element => {
drawpath(element);
});
}
linkEvent() {
if (! linkageFirst) {
linkageFirst = this;
} else if (this != linkageFirst) {
var pathElement = document.createElementNS(svgNamespace, 'path');
pathElement.id = "flow-"+this.id+"2"+linkageFirst.id;
pathElement.toElement = this;
pathElement.fromElement = linkageFirst;
pathElement.classList.add("flow");
this.wcForeFlow.push(pathElement);
linkageFirst.wcNextFlow.push(pathElement);
$("#connections").appendChild(pathElement);
drawpath(pathElement);
linkageFirst = null;
}
}
generateNextCode() {
var code = ""
this.wcNextFlow.forEach((path) => {
code += path.toElement.generateCode();
});
return code;
}
stopDragging(event) {
document.onmousemove = null;
document.onmouseup = null;
}
}
class WCOutput extends WCBlock {
constructor() {
super();
}
generateCode() {
return `
printf("${this.querySelector("textarea").value}");
` + this.generateNextCode();
}
}
class WCStart extends WCBlock {
constructor() {
super();
}
generateCode() {
var code = `
// --- WiseCode® ---
int main() {
`;
return code + this.generateNextCode() + `
}
// --- END ---
`;
}
}
class WCEnd extends WCBlock {
constructor() {
super();
}
generateCode() {
return `
// WCEnd
exit(0);
`;
}
}
class WCProgram extends HTMLElement {
constructor() {
super();
this.counter = 0;
}
build() {
this.startNode = document.importNode($("#wc-start").content, true)
this.endNode = document.importNode($("#wc-end").content, true)
this.startNode.firstElementChild.id = "main-start";
this.endNode.firstElementChild.id = "main-end";
this.appendChild(this.startNode)
this.appendChild(this.endNode)
}
newDecisionNode() {
this.counter ++;
const decisionNode = document.importNode($("#wc-decision").content, true)
decisionNode.firstElementChild.id = "decision"+this.counter;
this.appendChild(decisionNode);
}
newOutputNode() {
const outputNode = document.importNode($("#wc-output").content, true)
outputNode.firstElementChild.id = "output"+this.counter;
this.appendChild(outputNode);
}
newDataNode() {
const dataNode = document.importNode($("#wc-datum").content, true)
dataNode.firstElementChild.id = "data"+this.counter;
this.appendChild(dataNode);
}
generateCode() {
$("#c-code").value = this.firstElementChild.generateCode();
}
}
class WCDecision extends WCBlock {
constructor() {
super();
}
generateCode() {
return `
if (condition) goto next; else goto alt;
`;
}
}
class WCDatum extends WCBlock {
constructor() {
super();
}
}
function drawpath(path) {
const rectFrom = path.fromElement.querySelector(".hook").getBoundingClientRect();
const rectTo = path.toElement.querySelector(".hook").getBoundingClientRect();
const [fromX, fromY] = [(rectFrom.left + rectFrom.right) / 2, (rectFrom.top + rectFrom.bottom) / 2];
const [toX, toY] = [(rectTo.left + rectTo.right) / 2, (rectTo.top + rectTo.bottom) / 2];
path.setAttribute("d",
`M ${fromX} ${fromY} \
C\
${toX} ${fromY} \
${fromX} ${toY} \
${toX} ${toY} \
`
);
}
customElements.define("wc-start", WCStart);
customElements.define("wc-end", WCEnd);
customElements.define("wc-program", WCProgram);
customElements.define("wc-output", WCOutput);
customElements.define("wc-decision", WCDecision);
customElements.define("wc-datum", WCDatum);
function generateCode() {
const text = $("#main-start").generateCode()
$("#c-code").innerHTML = text;
}
function main() {
$("#main-program").build();
}
+118
View File
@@ -0,0 +1,118 @@
:root {
--grid-size: 25px;
}
wc-start, wc-end, wc-output, wc-decision, wc-datum {
border: 0.1rem solid black;
padding: .5rem;
/* WARNING: margin adds up every event handle when dragging the element, any margin set should be set outside*/
margin: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
user-select: none;
}
wc-start, wc-end {
border-radius: 1rem;
}
wc-output {
border-radius: 0;
background-color: #a0a0f0;
transform: skewX(-20deg);
}
wc-output * {
transform: skewX(20deg);
}
wc-decision {
background-color: #f0f0a0;
border-radius: 0;
aspect-ratio: 1;
transform: rotate(45deg);
display: block;
}
wc-decision * {
transform: rotate(-45deg);
}
wc-decision #hook-fore {
position: absolute;
top: 0;
left: 0;
}
wc-decision #hook-out1 {
position: absolute;
top:100%;
left:100%;
}
wc-decision #hook-out0 {
position: absolute;
top: 0%;
left: 100%;
}
wc-decision #hook-test {
align-self: center;
position: absolute;
top: 100%;
left: 0%;
}
wc-datum {
background-color: #2020f0;
color: white;
}
div#draw-area {
height: 100vh;
width: 100vw;
background-size: var(--grid-size) var(--grid-size);
background-image:
linear-gradient(to right, grey 1px, transparent 1px),
linear-gradient(to bottom, grey 1px, transparent 1px);
}
svg#connections {
z-index: -1;
background: transparent;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
path.flow {
fill: none;
stroke: red;
stroke-width: 5;
}
button.hook {
display: block;
width: 2em;
height: 2em;
border-radius: 1em;
contain: layout;
}
wc-end {
background-color: #a0ffa0;
}
wc-start {
background-color: #ffa0a0;
}
textarea {
white-space: pre-wrap;
}