From 6b0f1726e2d0d2637da3faa6dc8f4abeb235e295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Baumg=C3=A4rtner?= Date: Sun, 23 Apr 2023 13:42:02 +0200 Subject: [PATCH] added proof-of-concept base code provided by docent --- src/app.html | 59 +++++++++++ src/routes/+page.svelte | 2 - static/js/editor.js | 73 ++++++++++++++ static/js/preview.js | 31 ++++++ static/js/webgl.js | 214 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 static/js/editor.js create mode 100644 static/js/preview.js create mode 100644 static/js/webgl.js diff --git a/src/app.html b/src/app.html index effe0d0..ba06b64 100644 --- a/src/app.html +++ b/src/app.html @@ -5,8 +5,67 @@ %sveltekit.head% +
%sveltekit.body%
+
+ + + + + + + + + + + + + +
+ + + + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5982b0a..e69de29 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +0,0 @@ -

Welcome to SvelteKit

-

Visit kit.svelte.dev to read the documentation

diff --git a/static/js/editor.js b/static/js/editor.js new file mode 100644 index 0000000..97d09bc --- /dev/null +++ b/static/js/editor.js @@ -0,0 +1,73 @@ +(function () { + const svg = document.querySelector("svg"); + const path = svg.querySelector("path"); + const circles = toArray(svg.querySelectorAll("circle")); + let draggedCircle = null; + + function update() { + const points = circles.map((circle) => ({ + x: parseFloat(circle.getAttribute("cx")), + y: parseFloat(circle.getAttribute("cy")), + })); + + path.setAttribute("d", toSvgPath(points)); + + const event = new CustomEvent("myPointsChanged", { detail: points }); + document.dispatchEvent(event); + + make_base(); + } + + /** + * Event handlers + */ + + svg.addEventListener("mousedown", (event) => { + if (circles.includes(event.target)) { + draggedCircle = event.target; + } + }); + + document.addEventListener("mousemove", (event) => { + if (draggedCircle) { + const { height, left, top, width } = svg.getBoundingClientRect(); + const x = clamp(event.clientX - left, 0, width); + const y = clamp(event.clientY - top, 0, height); + + draggedCircle.setAttribute("cx", x); + draggedCircle.setAttribute("cy", y); + update(); + } + }); + + document.addEventListener("mouseup", () => { + draggedCircle = null; + }); + + document.addEventListener("DOMContentLoaded", () => { + update(); + }); + + /** + * Helpers + */ + + function clamp(value, min, max) { + return Math.max(min, Math.min(value, max)); + } + + function toArray(value) { + return Array.prototype.slice.call(value); + } + + function toSvgOperation(operation, { x, y }) { + return `${operation} ${x} ${y}`; + } + + function toSvgPath(points) { + return [ + toSvgOperation("M", points[points.length - 1]), + ...points.map((point) => toSvgOperation("L", point)), + ].join(" "); + } +})(); diff --git a/static/js/preview.js b/static/js/preview.js new file mode 100644 index 0000000..a48d8bc --- /dev/null +++ b/static/js/preview.js @@ -0,0 +1,31 @@ +(function () { + const image = document.querySelector(".myPreview__image"); + + function toCssMatrix(transform) { + return `matrix3d(${toMatrix(transform.coeffs).join(",")})`; + } + + function toMatrix(t) { + // prettier-ignore + return [ + t[0], t[3], 0, t[6], + t[1], t[4], 0, t[7], + 0, 0, 1, 0, + t[2], t[5], 0, t[8] + ]; + } + + /** + * Event handlers + */ + + document.addEventListener("myPointsChanged", (event) => { + const points = event.detail.reduce( + (result, { x, y }) => [...result, x, y], + [] + ); + + const transform = new PerspT([300, 0, 0, 0, 0, 300, 300, 300], points); + image.style.transform = toCssMatrix(transform); + }); +})(); diff --git a/static/js/webgl.js b/static/js/webgl.js new file mode 100644 index 0000000..ca5d8c0 --- /dev/null +++ b/static/js/webgl.js @@ -0,0 +1,214 @@ +(function () { + const canvas = document.querySelector(".myCanvas"); + const image = document.querySelector(".myPreview__image"); + const gl = canvas.getContext("webgl", { + preserveDrawingBuffer: true + }); + + const plane = createPlane(gl); + const texture = createTexture(gl); + const shader = createImageShader(gl); + let matrix = []; + + function update() { + gl.clearColor(255.0, 255.0, 255.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + shader.use().setProjection(matrix).setTexture(texture); + plane.draw(shader); + } + + function loadTexture() { + drawToTexure(gl, texture, image); + update(); + } + + function toMatrix(t) { + // prettier-ignore + return [ + t[0], t[1], 0, t[2], + t[3], t[4], 0, t[5], + 0, 0, 1, 0, + t[6], t[7], 0, t[8] + ]; + } + + if (image.naturalWidth && image.naturalHeight) { + loadTexture(); + } else { + image.addEventListener("load", () => { + loadTexture(); + }); + } + + /** + * Event handlers + */ + + document.addEventListener("myPointsChanged", (event) => { + const points = event.detail.reduce( + (result, { x, y }) => [...result, x / 150 - 1, 1 - y / 150], + [] + ); + + const transform = new PerspT([1, -1, -1, -1, -1, 1, 1, 1], points); + matrix = toMatrix(transform.coeffs); + + update(); + }); + + /** + * Primitives + */ + + function createPlane(gl) { + const positionBuffer = createBuffer( + gl, + [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0] + ); + + return { + draw(shader) { + shader.bindPositionBuffer(positionBuffer); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + }; + } + + function createImageShader(gl) { + const vertexSource = ` + attribute vec4 aPosition; + + uniform mat4 uProjection; + + varying highp vec2 vTextureCoord; + + void main() { + vTextureCoord = vec2((aPosition.x - 1.0) * 0.5, (aPosition.y - 1.0) * 0.5); + + gl_Position = vec4(aPosition.x, aPosition.y, 0.0, 1.0) * uProjection; + } + `; + + const fragmentSource = ` + varying highp vec2 vTextureCoord; + + uniform sampler2D uSampler; + + void main() { + gl_FragColor = texture2D(uSampler, vTextureCoord); + } + `; + + const program = createProgram( + gl, + createShader(gl, vertexSource, gl.VERTEX_SHADER), + createShader(gl, fragmentSource, gl.FRAGMENT_SHADER) + ); + + const positionAttribute = gl.getAttribLocation(program, "aPosition"); + const projectionLocation = gl.getUniformLocation(program, "uProjection"); + const samplerLocation = gl.getUniformLocation(program, "uSampler"); + + const instance = { + use() { + gl.useProgram(program); + gl.enableVertexAttribArray(positionAttribute); + return instance; + }, + bindPositionBuffer(buffer) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0); + }, + setProjection(value) { + gl.uniformMatrix4fv(projectionLocation, false, value); + return instance; + }, + setTexture(value) { + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, value); + gl.uniform1i(samplerLocation, 0); + return instance; + }, + }; + + return instance; + } + + /** + * WebGL helpers + * https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial + */ + + function createBuffer(gl, positions) { + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); + + return positionBuffer; + } + + function createProgram(gl, vertexShader, fragmentShader) { + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const info = gl.getProgramInfoLog(program); + throw `Could not compile WebGL program. \n\n${info}`; + } + + return program; + } + + function createShader(gl, source, type) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const info = gl.getShaderInfoLog(shader); + throw `Could not compile WebGL program. \n\n${info}`; + } + + return shader; + } + + function createTexture(gl) { + const texture = gl.createTexture(); + const pixel = new Uint8Array([0, 0, 255, 255]); + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 1, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixel + ); + + return texture; + } + + function drawToTexure(gl, texture, source) { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source); + + if (isPowerOf2(source.naturalWidth) && isPowerOf2(source.naturalHeight)) { + gl.generateMipmap(gl.TEXTURE_2D); + } else { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + } + } + + function isPowerOf2(value) { + return (value & (value - 1)) === 0; + } +})();