From 8b5b777ca49684c4613fd62ee4985183fb30356d Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Mon, 26 Jan 2026 22:14:42 -0700 Subject: [PATCH 01/10] add webgl examples --- .gitignore | 1 + README.md | 4 + examples/webgl_renderlist.html | 22 +++ examples/webgl_renderlist.nim | 252 ++++++++++++++++++++++++++++++ src/figdraw/webgl/api.nim | 278 +++++++++++++++++++++++++++++++++ 5 files changed, 557 insertions(+) create mode 100644 examples/webgl_renderlist.html create mode 100644 examples/webgl_renderlist.nim create mode 100644 src/figdraw/webgl/api.nim diff --git a/.gitignore b/.gitignore index 3cd5d70..06d34f2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ nimbledeps /examples/emscripten/*.data /examples/emscripten/*.html !/examples/emscripten/emscripten.html +/examples/*.js diff --git a/README.md b/README.md index a971c7a..b8b1189 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,10 @@ For a complete working example (window + GL context + render loop), see: - `examples/opengl_windy_renderlist.nim` - `examples/sdl2_renderlist.nim` +For a Nim JS + WebGL example (no Windy), see: + +- `examples/webgl_renderlist.nim` (paired with `examples/webgl_renderlist.html`) + ## Run Tests Runs all tests + compiles all examples listed in `config.nims`: diff --git a/examples/webgl_renderlist.html b/examples/webgl_renderlist.html new file mode 100644 index 0000000..878fa45 --- /dev/null +++ b/examples/webgl_renderlist.html @@ -0,0 +1,22 @@ + + + + + + FigDraw WebGL RenderList (Nim JS) + + + + + + + diff --git a/examples/webgl_renderlist.nim b/examples/webgl_renderlist.nim new file mode 100644 index 0000000..90b0ecc --- /dev/null +++ b/examples/webgl_renderlist.nim @@ -0,0 +1,252 @@ +when not defined(js) and not defined(nimsuggest): + {.fatal: "This example requires the Nim JS backend (nim js).".} + +import std/[dom, jsconsole, jsffi] +import figdraw/webgl/api + +type + Rect = object + x, y, w, h: float32 + color: array[4, float32] + +const + baseWidth = 800'f32 + baseHeight = 600'f32 + +proc rgba(r, g, b: int; a: int = 255): array[4, float32] = + [ + r.float32 / 255'f32, + g.float32 / 255'f32, + b.float32 / 255'f32, + a.float32 / 255'f32, + ] + +proc makeRenderList(width, height: float32): seq[Rect] = + let sx = width / baseWidth + let sy = height / baseHeight + result = @[ + Rect( + x: 60'f32 * sx, + y: 60'f32 * sy, + w: 220'f32 * sx, + h: 140'f32 * sy, + color: rgba(220, 40, 40), + ), + Rect( + x: 320'f32 * sx, + y: 120'f32 * sy, + w: 220'f32 * sx, + h: 140'f32 * sy, + color: rgba(40, 180, 90), + ), + Rect( + x: 180'f32 * sx, + y: 300'f32 * sy, + w: 220'f32 * sx, + h: 140'f32 * sy, + color: rgba(60, 90, 220), + ), + ] + +proc appendRect(data: var seq[float32]; rect: Rect) = + let x0 = rect.x + let y0 = rect.y + let x1 = rect.x + rect.w + let y1 = rect.y + rect.h + let c = rect.color + + data.add x0 + data.add y0 + data.add c[0] + data.add c[1] + data.add c[2] + data.add c[3] + + data.add x1 + data.add y0 + data.add c[0] + data.add c[1] + data.add c[2] + data.add c[3] + + data.add x0 + data.add y1 + data.add c[0] + data.add c[1] + data.add c[2] + data.add c[3] + + data.add x0 + data.add y1 + data.add c[0] + data.add c[1] + data.add c[2] + data.add c[3] + + data.add x1 + data.add y0 + data.add c[0] + data.add c[1] + data.add c[2] + data.add c[3] + + data.add x1 + data.add y1 + data.add c[0] + data.add c[1] + data.add c[2] + data.add c[3] + +proc buildVertexData(rects: seq[Rect]): seq[float32] = + result = newSeqOfCap[float32](rects.len * 6 * 6) + for rect in rects: + appendRect(result, rect) + +proc compileShader( + gl: WebGLRenderingContext; + shaderType: GLenum; + source: cstring; +): WebGLShader = + let shader = gl.createShader(shaderType) + if shader.isNull or shader.isUndefined: + console.error("createShader failed") + return nil + + gl.shaderSource(shader, source) + gl.compileShader(shader) + if not gl.getShaderParameter(shader, COMPILE_STATUS): + console.error("shader compile failed:", gl.getShaderInfoLog(shader)) + gl.deleteShader(shader) + return nil + + result = shader + +proc linkProgram( + gl: WebGLRenderingContext; + vertexShader: WebGLShader; + fragmentShader: WebGLShader; +): WebGLProgram = + let program = gl.createProgram() + if program.isNull or program.isUndefined: + console.error("createProgram failed") + return nil + + gl.attachShader(program, vertexShader) + gl.attachShader(program, fragmentShader) + gl.linkProgram(program) + if not gl.getProgramParameter(program, LINK_STATUS): + console.error("program link failed:", gl.getProgramInfoLog(program)) + gl.deleteProgram(program) + return nil + + result = program + +proc main() = + let canvas = document.createElement("canvas").asCanvas + document.body.appendChild(canvas) + + document.body.style.margin = "0" + document.body.style.overflow = "hidden" + document.body.style.background = "#0c0f16" + canvas.style.display = "block" + + let gl = canvas.getContext("webgl") + if gl.isNull or gl.isUndefined: + console.error("WebGL not available") + return + + let vertexSource = cstring(""" + attribute vec2 a_position; + attribute vec4 a_color; + uniform vec2 u_resolution; + varying vec4 v_color; + + void main() { + vec2 zeroToOne = a_position / u_resolution; + vec2 zeroToTwo = zeroToOne * 2.0; + vec2 clip = zeroToTwo - 1.0; + gl_Position = vec4(clip * vec2(1, -1), 0.0, 1.0); + v_color = a_color; + } + """) + + let fragmentSource = cstring(""" + precision mediump float; + varying vec4 v_color; + + void main() { + gl_FragColor = v_color; + } + """) + + let vertexShader = compileShader(gl, VERTEX_SHADER, vertexSource) + let fragmentShader = compileShader(gl, FRAGMENT_SHADER, fragmentSource) + if vertexShader.isNull or fragmentShader.isNull: + return + + let program = linkProgram(gl, vertexShader, fragmentShader) + if program.isNull: + return + + let aPosition = gl.getAttribLocation(program, "a_position") + let aColor = gl.getAttribLocation(program, "a_color") + let uResolution = gl.getUniformLocation(program, "u_resolution") + + let vertexBuffer = gl.createBuffer() + if vertexBuffer.isNull: + console.error("createBuffer failed") + return + + var lastWidth = 0 + var lastHeight = 0 + var vertexCount = 0 + + proc updateGeometry(width, height: int) = + let rects = makeRenderList(width.float32, height.float32) + let data = buildVertexData(rects) + vertexCount = data.len div 6 + gl.bindBuffer(ARRAY_BUFFER, vertexBuffer) + gl.bufferData(ARRAY_BUFFER, newFloat32Array(data), STATIC_DRAW) + + proc draw(width, height: int) = + gl.viewport(0, 0, canvas.width, canvas.height) + gl.clearColor(1.0, 1.0, 1.0, 1.0) + gl.clear(COLOR_BUFFER_BIT) + + gl.useProgram(program) + gl.bindBuffer(ARRAY_BUFFER, vertexBuffer) + + let stride = GLsizei(6 * 4) + gl.enableVertexAttribArray(aPosition) + gl.vertexAttribPointer(aPosition, 2, FLOAT, false, stride, 0.GLintptr) + gl.enableVertexAttribArray(aColor) + gl.vertexAttribPointer(aColor, 4, FLOAT, false, stride, (2 * 4).GLintptr) + + gl.uniform2f(uResolution, width.float32, height.float32) + gl.drawArrays(TRIANGLES, 0, vertexCount.GLsizei) + + proc resizeAndRender() = + let dpr = if window.devicePixelRatio <= 0: 1.0 else: window.devicePixelRatio + let cssWidth = max(window.innerWidth, 1) + let cssHeight = max(window.innerHeight, 1) + + canvas.width = int(cssWidth.float * dpr) + canvas.height = int(cssHeight.float * dpr) + canvas.style.width = cstring($cssWidth & "px") + canvas.style.height = cstring($cssHeight & "px") + + if cssWidth != lastWidth or cssHeight != lastHeight: + lastWidth = cssWidth + lastHeight = cssHeight + updateGeometry(cssWidth, cssHeight) + + draw(cssWidth, cssHeight) + + proc onResize(e: Event) = + resizeAndRender() + + window.addEventListener("resize", onResize) + resizeAndRender() + +when isMainModule: + main() diff --git a/src/figdraw/webgl/api.nim b/src/figdraw/webgl/api.nim new file mode 100644 index 0000000..3b157c7 --- /dev/null +++ b/src/figdraw/webgl/api.nim @@ -0,0 +1,278 @@ +when not defined(js) and not defined(nimsuggest): + {.fatal: "figdraw/webgl/api requires the Nim JS backend.".} + +import std/[dom, jsffi] + +type + GLenum* = uint32 + GLboolean* = bool + GLbitfield* = uint32 + GLbyte* = int8 + GLshort* = int16 + GLint* = int32 + GLsizei* = int32 + # WebGL expects numeric byte offsets; keep these 32-bit to avoid JS BigInt. + GLintptr* = int32 + GLsizeiptr* = int32 + GLubyte* = uint8 + GLushort* = uint16 + GLuint* = uint32 + GLfloat* = float32 + GLclampf* = float32 + GLint64* = int64 + GLuint64* = uint64 + +type + WebGLRenderingContext* {.importc.} = ref object of JsRoot + WebGL2RenderingContext* {.importc.} = ref object of WebGLRenderingContext + WebGLActiveInfo* {.importc.} = ref object of JsRoot + WebGLBuffer* {.importc.} = ref object of JsRoot + WebGLContextEvent* {.importc.} = ref object of Event + WebGLFramebuffer* {.importc.} = ref object of JsRoot + WebGLProgram* {.importc.} = ref object of JsRoot + WebGLQuery* {.importc.} = ref object of JsRoot + WebGLRenderbuffer* {.importc.} = ref object of JsRoot + WebGLSampler* {.importc.} = ref object of JsRoot + WebGLShader* {.importc.} = ref object of JsRoot + WebGLShaderPrecisionFormat* {.importc.} = ref object of JsRoot + WebGLSync* {.importc.} = ref object of JsRoot + WebGLTexture* {.importc.} = ref object of JsRoot + WebGLTransformFeedback* {.importc.} = ref object of JsRoot + WebGLUniformLocation* {.importc.} = ref object of JsRoot + WebGLVertexArrayObject* {.importc.} = ref object of JsRoot + + HTMLCanvasElement* {.importc.} = ref object of Element + width*: int + height*: int + + Float32Array* {.importc.} = ref object of JsRoot + Uint16Array* {.importc.} = ref object of JsRoot + Uint32Array* {.importc.} = ref object of JsRoot + +type + WebGLExtension* = ref object of JsRoot + AngleInstancedArrays* = WebGLExtension + ExtBlendMinmax* = WebGLExtension + ExtColorBufferFloat* = WebGLExtension + ExtColorBufferHalfFloat* = WebGLExtension + ExtDisjointTimerQuery* = WebGLExtension + ExtFloatBlend* = WebGLExtension + ExtFragDepth* = WebGLExtension + ExtShaderTextureLod* = WebGLExtension + ExtSRgb* = WebGLExtension + ExtTextureCompressionBptc* = WebGLExtension + ExtTextureCompressionRgtc* = WebGLExtension + ExtTextureFilterAnisotropic* = WebGLExtension + ExtTextureNorm16* = WebGLExtension + KhrParallelShaderCompile* = WebGLExtension + OesElementIndexUint* = WebGLExtension + OesFboRenderMipmap* = WebGLExtension + OesStandardDerivatives* = WebGLExtension + OesTextureFloat* = WebGLExtension + OesTextureFloatLinear* = WebGLExtension + OesTextureHalfFloat* = WebGLExtension + OesTextureHalfFloatLinear* = WebGLExtension + OesVertexArrayObject* = WebGLExtension + OvrMultiview2* = WebGLExtension + WebglColorBufferFloat* = WebGLExtension + WebglCompressedTextureAstc* = WebGLExtension + WebglCompressedTextureEtc* = WebGLExtension + WebglCompressedTextureEtc1* = WebGLExtension + WebglCompressedTexturePvrtc* = WebGLExtension + WebglCompressedTextureS3tc* = WebGLExtension + WebglCompressedTextureS3tcSrgb* = WebGLExtension + WebglDebugRendererInfo* = WebGLExtension + WebglDebugShaders* = WebGLExtension + WebglDepthTexture* = WebGLExtension + WebglDrawBuffers* = WebGLExtension + WebglLoseContext* = WebGLExtension + WebglMultiDraw* = WebGLExtension + +const + webglContextLost* = "webglcontextlost" + webglContextRestored* = "webglcontextrestored" + webglContextCreationError* = "webglcontextcreationerror" + + extAngleInstancedArrays* = "ANGLE_instanced_arrays" + extBlendMinmax* = "EXT_blend_minmax" + extColorBufferFloat* = "EXT_color_buffer_float" + extColorBufferHalfFloat* = "EXT_color_buffer_half_float" + extDisjointTimerQuery* = "EXT_disjoint_timer_query" + extFloatBlend* = "EXT_float_blend" + extFragDepth* = "EXT_frag_depth" + extShaderTextureLod* = "EXT_shader_texture_lod" + extSRgb* = "EXT_sRGB" + extTextureCompressionBptc* = "EXT_texture_compression_bptc" + extTextureCompressionRgtc* = "EXT_texture_compression_rgtc" + extTextureFilterAnisotropic* = "EXT_texture_filter_anisotropic" + extTextureNorm16* = "EXT_texture_norm16" + khrParallelShaderCompile* = "KHR_parallel_shader_compile" + oesElementIndexUint* = "OES_element_index_uint" + oesFboRenderMipmap* = "OES_fbo_render_mipmap" + oesStandardDerivatives* = "OES_standard_derivatives" + oesTextureFloat* = "OES_texture_float" + oesTextureFloatLinear* = "OES_texture_float_linear" + oesTextureHalfFloat* = "OES_texture_half_float" + oesTextureHalfFloatLinear* = "OES_texture_half_float_linear" + oesVertexArrayObject* = "OES_vertex_array_object" + ovrMultiview2* = "OVR_multiview2" + webglColorBufferFloat* = "WEBGL_color_buffer_float" + webglCompressedTextureAstc* = "WEBGL_compressed_texture_astc" + webglCompressedTextureEtc* = "WEBGL_compressed_texture_etc" + webglCompressedTextureEtc1* = "WEBGL_compressed_texture_etc1" + webglCompressedTexturePvrtc* = "WEBGL_compressed_texture_pvrtc" + webglCompressedTextureS3tc* = "WEBGL_compressed_texture_s3tc" + webglCompressedTextureS3tcSrgb* = "WEBGL_compressed_texture_s3tc_srgb" + webglDebugRendererInfo* = "WEBGL_debug_renderer_info" + webglDebugShaders* = "WEBGL_debug_shaders" + webglDepthTexture* = "WEBGL_depth_texture" + webglDrawBuffers* = "WEBGL_draw_buffers" + webglLoseContext* = "WEBGL_lose_context" + webglMultiDraw* = "WEBGL_multi_draw" + + ARRAY_BUFFER* = 0x8892.GLenum + STATIC_DRAW* = 0x88E4.GLenum + FLOAT* = 0x1406.GLenum + TRIANGLES* = 0x0004.GLenum + COLOR_BUFFER_BIT* = 0x4000.GLenum + VERTEX_SHADER* = 0x8B31.GLenum + FRAGMENT_SHADER* = 0x8B30.GLenum + COMPILE_STATUS* = 0x8B81.GLenum + LINK_STATUS* = 0x8B82.GLenum + +proc asCanvas*(el: Element): HTMLCanvasElement = + cast[HTMLCanvasElement](el) + +proc newFloat32Array*(data: openArray[float32]): Float32Array + {.importjs: "new Float32Array(#)".} + +proc newUint16Array*(data: openArray[uint16]): Uint16Array + {.importjs: "new Uint16Array(#)".} + +proc newUint32Array*(data: openArray[uint32]): Uint32Array + {.importjs: "new Uint32Array(#)".} + +proc getContext*(canvas: HTMLCanvasElement; + contextId: cstring): WebGLRenderingContext + {.importjs: "#.getContext(#)".} + +proc getContext*(canvas: HTMLCanvasElement; contextId: cstring; + options: JsObject): WebGLRenderingContext + {.importjs: "#.getContext(#, #)".} + +proc getExtension*(gl: WebGLRenderingContext; name: cstring): WebGLExtension + {.importjs: "#.getExtension(#)".} + +proc isContextLost*(gl: WebGLRenderingContext): bool + {.importjs: "#.isContextLost()".} + +proc statusMessage*(ev: WebGLContextEvent): cstring + {.importjs: "#.statusMessage".} + +proc canvas*(gl: WebGLRenderingContext): HTMLCanvasElement + {.importjs: "#.canvas".} + +proc drawingBufferWidth*(gl: WebGLRenderingContext): int + {.importjs: "#.drawingBufferWidth".} + +proc drawingBufferHeight*(gl: WebGLRenderingContext): int + {.importjs: "#.drawingBufferHeight".} + +proc createShader*(gl: WebGLRenderingContext; shaderType: GLenum): WebGLShader + {.importjs: "#.createShader(#)".} + +proc shaderSource*(gl: WebGLRenderingContext; shader: WebGLShader; + source: cstring) + {.importjs: "#.shaderSource(#, #)".} + +proc compileShader*(gl: WebGLRenderingContext; shader: WebGLShader) + {.importjs: "#.compileShader(#)".} + +proc getShaderParameter*(gl: WebGLRenderingContext; shader: WebGLShader; + pname: GLenum): bool + {.importjs: "#.getShaderParameter(#, #)".} + +proc getShaderInfoLog*(gl: WebGLRenderingContext; shader: WebGLShader): cstring + {.importjs: "#.getShaderInfoLog(#)".} + +proc deleteShader*(gl: WebGLRenderingContext; shader: WebGLShader) + {.importjs: "#.deleteShader(#)".} + +proc createProgram*(gl: WebGLRenderingContext): WebGLProgram + {.importjs: "#.createProgram()".} + +proc attachShader*(gl: WebGLRenderingContext; program: WebGLProgram; + shader: WebGLShader) + {.importjs: "#.attachShader(#, #)".} + +proc linkProgram*(gl: WebGLRenderingContext; program: WebGLProgram) + {.importjs: "#.linkProgram(#)".} + +proc getProgramParameter*(gl: WebGLRenderingContext; program: WebGLProgram; + pname: GLenum): bool + {.importjs: "#.getProgramParameter(#, #)".} + +proc getProgramInfoLog*(gl: WebGLRenderingContext; + program: WebGLProgram): cstring + {.importjs: "#.getProgramInfoLog(#)".} + +proc deleteProgram*(gl: WebGLRenderingContext; program: WebGLProgram) + {.importjs: "#.deleteProgram(#)".} + +proc useProgram*(gl: WebGLRenderingContext; program: WebGLProgram) + {.importjs: "#.useProgram(#)".} + +proc getAttribLocation*(gl: WebGLRenderingContext; program: WebGLProgram; + name: cstring): GLint + {.importjs: "#.getAttribLocation(#, #)".} + +proc enableVertexAttribArray*(gl: WebGLRenderingContext; index: GLint) + {.importjs: "#.enableVertexAttribArray(#)".} + +proc vertexAttribPointer*( + gl: WebGLRenderingContext; + index: GLint; + size: GLint; + typ: GLenum; + normalized: bool; + stride: GLsizei; + offset: GLintptr; +) {.importjs: "#.vertexAttribPointer(#, #, #, #, #, #)".} + +proc getUniformLocation*( + gl: WebGLRenderingContext; + program: WebGLProgram; + name: cstring; +): WebGLUniformLocation {.importjs: "#.getUniformLocation(#, #)".} + +proc uniform2f*( + gl: WebGLRenderingContext; + location: WebGLUniformLocation; + v0: GLfloat; + v1: GLfloat; +) {.importjs: "#.uniform2f(#, #, #)".} + +proc createBuffer*(gl: WebGLRenderingContext): WebGLBuffer + {.importjs: "#.createBuffer()".} + +proc bindBuffer*(gl: WebGLRenderingContext; target: GLenum; buffer: WebGLBuffer) + {.importjs: "#.bindBuffer(#, #)".} + +proc bufferData*(gl: WebGLRenderingContext; target: GLenum; data: Float32Array; usage: GLenum) + {.importjs: "#.bufferData(#, #, #)".} + +proc deleteBuffer*(gl: WebGLRenderingContext; buffer: WebGLBuffer) + {.importjs: "#.deleteBuffer(#)".} + +proc clearColor*(gl: WebGLRenderingContext; r, g, b, a: GLclampf) + {.importjs: "#.clearColor(#, #, #, #)".} + +proc clear*(gl: WebGLRenderingContext; mask: GLbitfield) + {.importjs: "#.clear(#)".} + +proc viewport*(gl: WebGLRenderingContext; x, y: GLint; width, height: GLsizei) + {.importjs: "#.viewport(#, #, #, #)".} + +proc drawArrays*(gl: WebGLRenderingContext; mode: GLenum; first: GLint; + count: GLsizei) + {.importjs: "#.drawArrays(#, #, #)".} From ca0f3983f103f66b4b5adf3988bee7f58c7c214e Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Mon, 26 Jan 2026 22:15:58 -0700 Subject: [PATCH 02/10] add webgl examples --- examples/webgl_renderlist.nim | 78 ++++++++++++++++++++++++----------- src/figdraw/webgl/api.nim | 9 ++++ 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/examples/webgl_renderlist.nim b/examples/webgl_renderlist.nim index 90b0ecc..c5b1e7d 100644 --- a/examples/webgl_renderlist.nim +++ b/examples/webgl_renderlist.nim @@ -21,32 +21,61 @@ proc rgba(r, g, b: int; a: int = 255): array[4, float32] = a.float32 / 255'f32, ] +proc addRect(list: var seq[Rect]; x, y, w, h: float32; + color: array[4, float32]) = + list.add Rect(x: x, y: y, w: w, h: h, color: color) + +proc addBorder(list: var seq[Rect]; x, y, w, h: float32; weight: float32; + color: array[4, float32]) = + list.add Rect( + x: x - weight, + y: y - weight, + w: w + weight * 2'f32, + h: h + weight * 2'f32, + color: color, + ) + +proc addShadow(list: var seq[Rect]; x, y, w, h: float32; offsetX, + offsetY: float32;spread: float32; color: array[4, float32]) = + list.add Rect( + x: x + offsetX - spread, + y: y + offsetY - spread, + w: w + spread * 2'f32, + h: h + spread * 2'f32, + color: color, + ) + proc makeRenderList(width, height: float32): seq[Rect] = let sx = width / baseWidth let sy = height / baseHeight - result = @[ - Rect( - x: 60'f32 * sx, - y: 60'f32 * sy, - w: 220'f32 * sx, - h: 140'f32 * sy, - color: rgba(220, 40, 40), - ), - Rect( - x: 320'f32 * sx, - y: 120'f32 * sy, - w: 220'f32 * sx, - h: 140'f32 * sy, - color: rgba(40, 180, 90), - ), - Rect( - x: 180'f32 * sx, - y: 300'f32 * sy, - w: 220'f32 * sx, - h: 140'f32 * sy, - color: rgba(60, 90, 220), - ), - ] + let s = min(sx, sy) + let borderWeight = 5'f32 * s + let shadowOffsetX = 10'f32 * sx + let shadowOffsetY = 10'f32 * sy + let shadowSpread = 10'f32 * s + + result = @[] + + let r1x = 60'f32 * sx + let r1y = 60'f32 * sy + let r1w = 220'f32 * sx + let r1h = 140'f32 * sy + addBorder(result, r1x, r1y, r1w, r1h, borderWeight, rgba(0, 0, 0)) + addRect(result, r1x, r1y, r1w, r1h, rgba(220, 40, 40)) + + let r2x = 320'f32 * sx + let r2y = 120'f32 * sy + let r2w = 220'f32 * sx + let r2h = 140'f32 * sy + addShadow(result, r2x, r2y, r2w, r2h, shadowOffsetX, shadowOffsetY, + shadowSpread, rgba(0, 0, 0, 55)) + addRect(result, r2x, r2y, r2w, r2h, rgba(40, 180, 90)) + + let r3x = 180'f32 * sx + let r3y = 300'f32 * sy + let r3w = 220'f32 * sx + let r3h = 140'f32 * sy + addRect(result, r3x, r3y, r3w, r3h, rgba(60, 90, 220)) proc appendRect(data: var seq[float32]; rect: Rect) = let x0 = rect.x @@ -155,6 +184,9 @@ proc main() = console.error("WebGL not available") return + gl.enable(BLEND) + gl.blendFunc(SRC_ALPHA, ONE_MINUS_SRC_ALPHA) + let vertexSource = cstring(""" attribute vec2 a_position; attribute vec4 a_color; diff --git a/src/figdraw/webgl/api.nim b/src/figdraw/webgl/api.nim index 3b157c7..5d67898 100644 --- a/src/figdraw/webgl/api.nim +++ b/src/figdraw/webgl/api.nim @@ -135,6 +135,9 @@ const FLOAT* = 0x1406.GLenum TRIANGLES* = 0x0004.GLenum COLOR_BUFFER_BIT* = 0x4000.GLenum + BLEND* = 0x0BE2.GLenum + SRC_ALPHA* = 0x0302.GLenum + ONE_MINUS_SRC_ALPHA* = 0x0303.GLenum VERTEX_SHADER* = 0x8B31.GLenum FRAGMENT_SHADER* = 0x8B30.GLenum COMPILE_STATUS* = 0x8B81.GLenum @@ -270,6 +273,12 @@ proc clearColor*(gl: WebGLRenderingContext; r, g, b, a: GLclampf) proc clear*(gl: WebGLRenderingContext; mask: GLbitfield) {.importjs: "#.clear(#)".} +proc enable*(gl: WebGLRenderingContext; cap: GLenum) + {.importjs: "#.enable(#)".} + +proc blendFunc*(gl: WebGLRenderingContext; sfactor: GLenum; dfactor: GLenum) + {.importjs: "#.blendFunc(#, #)".} + proc viewport*(gl: WebGLRenderingContext; x, y: GLint; width, height: GLsizei) {.importjs: "#.viewport(#, #, #, #)".} From a7854b3cc777461b8e2a6f438a0b05a2efc190e8 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:11:37 -0700 Subject: [PATCH 03/10] add webgl --- examples/webgl_renderlist.nim | 330 ++++--------- src/figdraw/common/fontutils.nim | 2 +- src/figdraw/common/fontutils_js.nim | 28 ++ src/figdraw/common/imgutils.nim | 2 +- src/figdraw/common/imgutils_js.nim | 28 ++ src/figdraw/common/rchannels_js.nim | 14 + src/figdraw/common/shared.nim | 15 +- src/figdraw/common/transfer.nim | 5 +- src/figdraw/commons.nim | 14 +- src/figdraw/figbasics.nim | 20 +- src/figdraw/fignodes.nim | 15 +- src/figdraw/opengl/buffers.nim | 50 +- src/figdraw/opengl/glapi.nim | 327 ++++++++++++ src/figdraw/opengl/glcontext.nim | 573 ++++++++++++++-------- src/figdraw/opengl/glsl/webgl2/atlas.frag | 126 +++++ src/figdraw/opengl/glsl/webgl2/atlas.vert | 32 ++ src/figdraw/opengl/glsl/webgl2/mask.frag | 64 +++ src/figdraw/opengl/renderer.nim | 156 +++--- src/figdraw/opengl/shaders.nim | 530 ++++++++++++-------- src/figdraw/opengl/textures.nim | 204 +++++--- src/figdraw/utils/chronicles_stub.nim | 17 + src/figdraw/utils/drawextras.nim | 2 +- src/figdraw/utils/drawshadows.nim | 30 +- src/figdraw/utils/drawutils.nim | 11 +- src/figdraw/utils/glutils.nim | 4 +- src/figdraw/utils/logging.nim | 6 + src/figdraw/webgl/api.nim | 14 +- 27 files changed, 1776 insertions(+), 843 deletions(-) create mode 100644 src/figdraw/common/fontutils_js.nim create mode 100644 src/figdraw/common/imgutils_js.nim create mode 100644 src/figdraw/common/rchannels_js.nim create mode 100644 src/figdraw/opengl/glapi.nim create mode 100644 src/figdraw/opengl/glsl/webgl2/atlas.frag create mode 100644 src/figdraw/opengl/glsl/webgl2/atlas.vert create mode 100644 src/figdraw/opengl/glsl/webgl2/mask.frag create mode 100644 src/figdraw/utils/chronicles_stub.nim create mode 100644 src/figdraw/utils/logging.nim diff --git a/examples/webgl_renderlist.nim b/examples/webgl_renderlist.nim index c5b1e7d..c6ba6ed 100644 --- a/examples/webgl_renderlist.nim +++ b/examples/webgl_renderlist.nim @@ -2,176 +2,72 @@ when not defined(js) and not defined(nimsuggest): {.fatal: "This example requires the Nim JS backend (nim js).".} import std/[dom, jsconsole, jsffi] -import figdraw/webgl/api - -type - Rect = object - x, y, w, h: float32 - color: array[4, float32] - -const - baseWidth = 800'f32 - baseHeight = 600'f32 - -proc rgba(r, g, b: int; a: int = 255): array[4, float32] = - [ - r.float32 / 255'f32, - g.float32 / 255'f32, - b.float32 / 255'f32, - a.float32 / 255'f32, - ] - -proc addRect(list: var seq[Rect]; x, y, w, h: float32; - color: array[4, float32]) = - list.add Rect(x: x, y: y, w: w, h: h, color: color) - -proc addBorder(list: var seq[Rect]; x, y, w, h: float32; weight: float32; - color: array[4, float32]) = - list.add Rect( - x: x - weight, - y: y - weight, - w: w + weight * 2'f32, - h: h + weight * 2'f32, - color: color, - ) - -proc addShadow(list: var seq[Rect]; x, y, w, h: float32; offsetX, - offsetY: float32;spread: float32; color: array[4, float32]) = - list.add Rect( - x: x + offsetX - spread, - y: y + offsetY - spread, - w: w + spread * 2'f32, - h: h + spread * 2'f32, - color: color, - ) - -proc makeRenderList(width, height: float32): seq[Rect] = - let sx = width / baseWidth - let sy = height / baseHeight - let s = min(sx, sy) - let borderWeight = 5'f32 * s - let shadowOffsetX = 10'f32 * sx - let shadowOffsetY = 10'f32 * sy - let shadowSpread = 10'f32 * s - - result = @[] - - let r1x = 60'f32 * sx - let r1y = 60'f32 * sy - let r1w = 220'f32 * sx - let r1h = 140'f32 * sy - addBorder(result, r1x, r1y, r1w, r1h, borderWeight, rgba(0, 0, 0)) - addRect(result, r1x, r1y, r1w, r1h, rgba(220, 40, 40)) - - let r2x = 320'f32 * sx - let r2y = 120'f32 * sy - let r2w = 220'f32 * sx - let r2h = 140'f32 * sy - addShadow(result, r2x, r2y, r2w, r2h, shadowOffsetX, shadowOffsetY, - shadowSpread, rgba(0, 0, 0, 55)) - addRect(result, r2x, r2y, r2w, r2h, rgba(40, 180, 90)) - - let r3x = 180'f32 * sx - let r3y = 300'f32 * sy - let r3w = 220'f32 * sx - let r3h = 140'f32 * sy - addRect(result, r3x, r3y, r3w, r3h, rgba(60, 90, 220)) - -proc appendRect(data: var seq[float32]; rect: Rect) = - let x0 = rect.x - let y0 = rect.y - let x1 = rect.x + rect.w - let y1 = rect.y + rect.h - let c = rect.color - - data.add x0 - data.add y0 - data.add c[0] - data.add c[1] - data.add c[2] - data.add c[3] - - data.add x1 - data.add y0 - data.add c[0] - data.add c[1] - data.add c[2] - data.add c[3] - - data.add x0 - data.add y1 - data.add c[0] - data.add c[1] - data.add c[2] - data.add c[3] - - data.add x0 - data.add y1 - data.add c[0] - data.add c[1] - data.add c[2] - data.add c[3] - - data.add x1 - data.add y0 - data.add c[0] - data.add c[1] - data.add c[2] - data.add c[3] - - data.add x1 - data.add y1 - data.add c[0] - data.add c[1] - data.add c[2] - data.add c[3] - -proc buildVertexData(rects: seq[Rect]): seq[float32] = - result = newSeqOfCap[float32](rects.len * 6 * 6) - for rect in rects: - appendRect(result, rect) - -proc compileShader( - gl: WebGLRenderingContext; - shaderType: GLenum; - source: cstring; -): WebGLShader = - let shader = gl.createShader(shaderType) - if shader.isNull or shader.isUndefined: - console.error("createShader failed") - return nil - - gl.shaderSource(shader, source) - gl.compileShader(shader) - if not gl.getShaderParameter(shader, COMPILE_STATUS): - console.error("shader compile failed:", gl.getShaderInfoLog(shader)) - gl.deleteShader(shader) - return nil - - result = shader - -proc linkProgram( - gl: WebGLRenderingContext; - vertexShader: WebGLShader; - fragmentShader: WebGLShader; -): WebGLProgram = - let program = gl.createProgram() - if program.isNull or program.isUndefined: - console.error("createProgram failed") - return nil - - gl.attachShader(program, vertexShader) - gl.attachShader(program, fragmentShader) - gl.linkProgram(program) - if not gl.getProgramParameter(program, LINK_STATUS): - console.error("program link failed:", gl.getProgramInfoLog(program)) - gl.deleteProgram(program) - return nil - - result = program +import chroma + +import figdraw/commons +import figdraw/fignodes +import figdraw/opengl/renderer as glrenderer +import figdraw/opengl/glapi +import figdraw/utils/glutils +import figdraw/webgl/api as webgl + +proc makeRenderTree*(w, h: float32): Renders = + var list = RenderList() + + let rootIdx = list.addRoot(Fig( + kind: nkRectangle, + childCount: 0, + zlevel: 0.ZLevel, + screenBox: rect(0, 0, w, h), + fill: rgba(255, 255, 255, 255).color, + )) + + list.addChild(rootIdx, Fig( + kind: nkRectangle, + childCount: 0, + zlevel: 0.ZLevel, + corners: [10.0'f32, 20.0, 30.0, 40.0], + screenBox: rect(60, 60, 220, 140), + fill: rgba(220, 40, 40, 255).color, + stroke: RenderStroke(weight: 5.0, color: rgba(0, 0, 0, 255).color) + )) + list.addChild(rootIdx, Fig( + kind: nkRectangle, + childCount: 0, + zlevel: 0.ZLevel, + screenBox: rect(320, 120, 220, 140), + fill: rgba(40, 180, 90, 255).color, + shadows: [ + RenderShadow( + style: DropShadow, + blur: 10, + spread: 10, + x: 10, + y: 10, + color: rgba(0, 0, 0, 55).color, + ), + RenderShadow(), + RenderShadow(), + RenderShadow(), + ], + )) + list.addChild(rootIdx, Fig( + kind: nkRectangle, + childCount: 0, + zlevel: 0.ZLevel, + screenBox: rect(180, 300, 220, 140), + fill: rgba(60, 90, 220, 255).color, + )) + + result = Renders(layers: initOrderedTable[ZLevel, RenderList]()) + result.layers[0.ZLevel] = list proc main() = - let canvas = document.createElement("canvas").asCanvas + app.running = true + app.autoUiScale = false + app.uiScale = 1.0 + + let canvas = webgl.asCanvas(document.createElement("canvas")) document.body.appendChild(canvas) document.body.style.margin = "0" @@ -179,100 +75,44 @@ proc main() = document.body.style.background = "#0c0f16" canvas.style.display = "block" - let gl = canvas.getContext("webgl") + let gl = cast[glapi.WebGL2RenderingContext](canvas.getContext("webgl2")) if gl.isNull or gl.isUndefined: - console.error("WebGL not available") - return - - gl.enable(BLEND) - gl.blendFunc(SRC_ALPHA, ONE_MINUS_SRC_ALPHA) - - let vertexSource = cstring(""" - attribute vec2 a_position; - attribute vec4 a_color; - uniform vec2 u_resolution; - varying vec4 v_color; - - void main() { - vec2 zeroToOne = a_position / u_resolution; - vec2 zeroToTwo = zeroToOne * 2.0; - vec2 clip = zeroToTwo - 1.0; - gl_Position = vec4(clip * vec2(1, -1), 0.0, 1.0); - v_color = a_color; - } - """) - - let fragmentSource = cstring(""" - precision mediump float; - varying vec4 v_color; - - void main() { - gl_FragColor = v_color; - } - """) - - let vertexShader = compileShader(gl, VERTEX_SHADER, vertexSource) - let fragmentShader = compileShader(gl, FRAGMENT_SHADER, fragmentSource) - if vertexShader.isNull or fragmentShader.isNull: - return - - let program = linkProgram(gl, vertexShader, fragmentShader) - if program.isNull: - return - - let aPosition = gl.getAttribLocation(program, "a_position") - let aColor = gl.getAttribLocation(program, "a_color") - let uResolution = gl.getUniformLocation(program, "u_resolution") - - let vertexBuffer = gl.createBuffer() - if vertexBuffer.isNull: - console.error("createBuffer failed") + console.error("WebGL2 not available") return - var lastWidth = 0 - var lastHeight = 0 - var vertexCount = 0 + setWebGLContext(gl) + startOpenGL(openglVersion) - proc updateGeometry(width, height: int) = - let rects = makeRenderList(width.float32, height.float32) - let data = buildVertexData(rects) - vertexCount = data.len div 6 - gl.bindBuffer(ARRAY_BUFFER, vertexBuffer) - gl.bufferData(ARRAY_BUFFER, newFloat32Array(data), STATIC_DRAW) - - proc draw(width, height: int) = - gl.viewport(0, 0, canvas.width, canvas.height) - gl.clearColor(1.0, 1.0, 1.0, 1.0) - gl.clear(COLOR_BUFFER_BIT) - - gl.useProgram(program) - gl.bindBuffer(ARRAY_BUFFER, vertexBuffer) - - let stride = GLsizei(6 * 4) - gl.enableVertexAttribArray(aPosition) - gl.vertexAttribPointer(aPosition, 2, FLOAT, false, stride, 0.GLintptr) - gl.enableVertexAttribArray(aColor) - gl.vertexAttribPointer(aColor, 4, FLOAT, false, stride, (2 * 4).GLintptr) + let renderer = glrenderer.newOpenGLRenderer( + atlasSize = 192, + pixelScale = 1.0, + ) - gl.uniform2f(uResolution, width.float32, height.float32) - gl.drawArrays(TRIANGLES, 0, vertexCount.GLsizei) + var lastCssSize = vec2(0.0'f32, 0.0'f32) + var renders = makeRenderTree(0.0'f32, 0.0'f32) proc resizeAndRender() = let dpr = if window.devicePixelRatio <= 0: 1.0 else: window.devicePixelRatio let cssWidth = max(window.innerWidth, 1) let cssHeight = max(window.innerHeight, 1) + app.pixelScale = dpr.float32 + renderer.ctx.pixelScale = app.pixelScale + canvas.width = int(cssWidth.float * dpr) canvas.height = int(cssHeight.float * dpr) canvas.style.width = cstring($cssWidth & "px") canvas.style.height = cstring($cssHeight & "px") - if cssWidth != lastWidth or cssHeight != lastHeight: - lastWidth = cssWidth - lastHeight = cssHeight - updateGeometry(cssWidth, cssHeight) + let cssSize = vec2(cssWidth.float32, cssHeight.float32) + if cssSize != lastCssSize: + lastCssSize = cssSize + renders = makeRenderTree(cssSize.x, cssSize.y) - draw(cssWidth, cssHeight) + renderer.renderFrame( + renders, + vec2(canvas.width.float32, canvas.height.float32), + ) proc onResize(e: Event) = resizeAndRender() diff --git a/src/figdraw/common/fontutils.nim b/src/figdraw/common/fontutils.nim index b7d2014..9cf0d5a 100644 --- a/src/figdraw/common/fontutils.nim +++ b/src/figdraw/common/fontutils.nim @@ -4,7 +4,7 @@ import std/isolation import pkg/vmath import pkg/pixie import pkg/pixie/fonts -import pkg/chronicles +import ../utils/logging import ./rchannels import ./imgutils diff --git a/src/figdraw/common/fontutils_js.nim b/src/figdraw/common/fontutils_js.nim new file mode 100644 index 0000000..6af8239 --- /dev/null +++ b/src/figdraw/common/fontutils_js.nim @@ -0,0 +1,28 @@ +import fonttypes +import uimaths + +type TypeFaceKinds* = enum + TTF + OTF + SVG + +type Box* = Rect + +proc getTypefaceImpl*(name: string): FontId = + FontId(0) + +proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = + FontId(0) + +proc getLineHeightImpl*(font: UiFont): float32 = + 0.0 + +proc getTypesetImpl*( + box: Box, + spans: openArray[(UiFont, string)], + hAlign = Left, + vAlign = Top, + minContent = false, + wrap = true, +): GlyphArrangement = + GlyphArrangement() diff --git a/src/figdraw/common/imgutils.nim b/src/figdraw/common/imgutils.nim index f65cc8a..c1fbe8c 100644 --- a/src/figdraw/common/imgutils.nim +++ b/src/figdraw/common/imgutils.nim @@ -4,7 +4,7 @@ import std/[isolation, locks, times] import pkg/vmath import pkg/pixie import pkg/pixie/fonts -import chronicles +import ../utils/logging import ./rchannels import ./formatflippy diff --git a/src/figdraw/common/imgutils_js.nim b/src/figdraw/common/imgutils_js.nim new file mode 100644 index 0000000..4b8384b --- /dev/null +++ b/src/figdraw/common/imgutils_js.nim @@ -0,0 +1,28 @@ +import std/hashes + +type + ImageId* = distinct Hash + + ImgObj* = object + id*: ImageId + + ImageChannel* = object + +var imageChan* = ImageChannel() + +proc `==`*(a, b: ImageId): bool {.borrow.} + +proc imgId*(name: string): ImageId = + hash(name).ImageId + +proc tryRecv*(ch: ImageChannel; msg: var ImgObj): bool = + false + +proc hasImage*(id: ImageId): bool = + false + +proc loadImage*(filePath: string): ImageId = + imgId(filePath) + +proc loadImage*(id: ImageId; image: auto) = + discard diff --git a/src/figdraw/common/rchannels_js.nim b/src/figdraw/common/rchannels_js.nim new file mode 100644 index 0000000..4346098 --- /dev/null +++ b/src/figdraw/common/rchannels_js.nim @@ -0,0 +1,14 @@ +type + RChan*[T] = object + +proc newRChan*[T](size: int = 0): RChan[T] = + RChan[T]() + +proc tryRecv*[T](ch: RChan[T], msg: var T): bool = + false + +proc send*[T](ch: var RChan[T], msg: T) = + discard + +proc push*[T](ch: var RChan[T], msg: T) = + discard diff --git a/src/figdraw/common/shared.nim b/src/figdraw/common/shared.nim index 6e26eda..3735317 100644 --- a/src/figdraw/common/shared.nim +++ b/src/figdraw/common/shared.nim @@ -1,10 +1,13 @@ import std/[sequtils, tables, hashes] import std/[unicode, strformat] -import std/os -import pkg/variant +when not defined(js): + import std/os +when not defined(js): + import pkg/variant export sequtils, strformat, tables, hashes -export variant +when not defined(js): + export variant import extras, uimaths export extras, uimaths @@ -56,7 +59,11 @@ type pixelScale*: float32 var - dataDirStr* {.runtimeVar.}: string = os.getCurrentDir() / "data" + dataDirStr* {.runtimeVar.}: string = + when defined(js): + "data" + else: + os.getCurrentDir() / "data" app* {.runtimeVar.} = AppState(running: true, uiScale: 1.0, autoUiScale: true, pixelScale: 1.0) diff --git a/src/figdraw/common/transfer.nim b/src/figdraw/common/transfer.nim index f6bbdbb..db04551 100644 --- a/src/figdraw/common/transfer.nim +++ b/src/figdraw/common/transfer.nim @@ -1,5 +1,6 @@ import std/sequtils -import stack_strings +when not defined(js): + import stack_strings import ../fignodes type RenderTree* = ref object @@ -105,7 +106,7 @@ proc copyInto*[N](uis: N): Renders = result.layers.sort( proc(x, y: auto): int = - cmp(x[0], y[0]) + cmp(x[0], y[0]) ) # echo "nodes:len: ", result.len() # printRenders(result) diff --git a/src/figdraw/commons.nim b/src/figdraw/commons.nim index 8082e4a..db151a9 100644 --- a/src/figdraw/commons.nim +++ b/src/figdraw/commons.nim @@ -1,13 +1,19 @@ import common/shared import common/uimaths -import common/rchannels +when defined(js): + import common/rchannels_js as rchannels +else: + import common/rchannels import common/transfer import common/appframes -import common/fontutils -import common/imgutils +when defined(js): + import common/fontutils_js as fontutils + import common/imgutils_js as imgutils +else: + import common/fontutils + import common/imgutils export shared, uimaths, rchannels export transfer, appframes export fontutils export imgutils - diff --git a/src/figdraw/figbasics.nim b/src/figdraw/figbasics.nim index 0263863..a9a88ab 100644 --- a/src/figdraw/figbasics.nim +++ b/src/figdraw/figbasics.nim @@ -1,12 +1,19 @@ import std/[options, hashes] -import chroma, stack_strings +import chroma +when not defined(js): + import stack_strings import common/uimaths import common/fonttypes -import common/imgutils +when defined(js): + import common/imgutils_js as imgutils +else: + import common/imgutils export uimaths, fonttypes, imgutils -export options, chroma, stack_strings +export options, chroma +when not defined(js): + export stack_strings const FigStringCap* {.intdefine.} = 48 @@ -14,9 +21,13 @@ const FigDrawNames* {.booldefine: "figdraw.names".}: bool = false type - FigName* = StackString[FigStringCap] FigID* = int64 +when defined(js): + type FigName* = string +else: + type FigName* = StackString[FigStringCap] + type Directions* = enum dTop @@ -75,4 +86,3 @@ type ImageStyle* = object color*: Color id*: ImageId - diff --git a/src/figdraw/fignodes.nim b/src/figdraw/fignodes.nim index f6aab6c..2974e6b 100644 --- a/src/figdraw/fignodes.nim +++ b/src/figdraw/fignodes.nim @@ -45,11 +45,16 @@ type proc `$`*(id: FigIdx): string = "FigIdx(" & $(int(id)) & ")" -proc toFigName*(s: string): FigName = - toStackString(s[0 ..< min(s.len(), s.len())], FigStringCap) - -proc toFigName*(s: FigName): FigName = - s +when defined(js): + proc toFigName*(s: string): FigName = + s +else: + proc toFigName*(s: string): FigName = + toStackString(s[0 ..< min(s.len(), s.len())], FigStringCap) + +when not defined(js): + proc toFigName*(s: FigName): FigName = + s proc `+`*(a, b: FigIdx): FigIdx {.borrow.} proc `<=`*(a, b: FigIdx): bool {.borrow.} diff --git a/src/figdraw/opengl/buffers.nim b/src/figdraw/opengl/buffers.nim index 94c4d78..420d134 100644 --- a/src/figdraw/opengl/buffers.nim +++ b/src/figdraw/opengl/buffers.nim @@ -1,4 +1,4 @@ -import opengl +import glapi type BufferKind* = enum @@ -16,7 +16,7 @@ type kind*: BufferKind normalized*: bool usage*: GLenum - bufferId*: GLuint + bufferId*: GlBufferId byteCapacity*: int func size*(componentType: GLenum): Positive = @@ -39,18 +39,42 @@ func componentCount*(bufferKind: BufferKind): Positive = of bkMAT3: 9 of bkMAT4: 16 -proc bindBufferData*(buffer: ptr Buffer, data: pointer) = - if buffer.bufferId == 0: - glGenBuffers(1, buffer.bufferId.addr) +when defined(js): + proc bindBufferData*[T](buffer: ptr Buffer, data: openArray[T]) = + if buffer.bufferId.isNil: + buffer.bufferId = glCreateBuffer() - let byteLength = - buffer.count * buffer.kind.componentCount() * buffer.componentType.size() - - glBindBuffer(buffer.target, buffer.bufferId) - if buffer.byteCapacity < byteLength: + let byteLength = + buffer.count * buffer.kind.componentCount() * buffer.componentType.size() let usage = if buffer.usage == 0.GLenum: GL_STATIC_DRAW else: buffer.usage - glBufferData(buffer.target, byteLength, nil, usage) + + glBindBuffer(buffer.target, buffer.bufferId) + if byteLength <= 0: + return + + when T is float32: + glBufferData(buffer.target, newFloat32Array(data), usage) + elif T is uint16: + glBufferData(buffer.target, newUint16Array(data), usage) + elif T is uint8: + glBufferData(buffer.target, newUint8Array(data), usage) + else: + {.fatal: "Unsupported buffer data type for WebGL.".} + buffer.byteCapacity = byteLength +else: + proc bindBufferData*(buffer: ptr Buffer, data: pointer) = + if buffer.bufferId == 0: + glGenBuffers(1, buffer.bufferId.addr) + + let byteLength = + buffer.count * buffer.kind.componentCount() * buffer.componentType.size() + + glBindBuffer(buffer.target, buffer.bufferId) + if buffer.byteCapacity < byteLength: + let usage = if buffer.usage == 0.GLenum: GL_STATIC_DRAW else: buffer.usage + glBufferData(buffer.target, byteLength, nil, usage) + buffer.byteCapacity = byteLength - if data != nil and byteLength > 0: - glBufferSubData(buffer.target, 0, byteLength, data) + if data != nil and byteLength > 0: + glBufferSubData(buffer.target, 0, byteLength, data) diff --git a/src/figdraw/opengl/glapi.nim b/src/figdraw/opengl/glapi.nim new file mode 100644 index 0000000..b11112f --- /dev/null +++ b/src/figdraw/opengl/glapi.nim @@ -0,0 +1,327 @@ +when defined(js): + import std/jsffi + import ../webgl/api as webgl + + export webgl.GLenum, webgl.GLboolean, webgl.GLbitfield, webgl.GLbyte, + webgl.GLshort, webgl.GLint, webgl.GLsizei, webgl.GLintptr, webgl.GLsizeiptr, + webgl.GLubyte, webgl.GLushort, webgl.GLuint, webgl.GLfloat, webgl.GLclampf + + type + GlBufferId* = webgl.WebGLBuffer + GlTextureId* = webgl.WebGLTexture + GlProgramId* = webgl.WebGLProgram + GlShaderId* = webgl.WebGLShader + GlVertexArrayId* = webgl.WebGLVertexArrayObject + GlFramebufferId* = webgl.WebGLFramebuffer + GlUniformLocation* = webgl.WebGLUniformLocation + WebGL2RenderingContext* = webgl.WebGL2RenderingContext + + const + GL_FALSE* = false + GL_TRUE* = true + + GL_NO_ERROR* = 0.GLenum + + GL_BYTE* = 0x1400.GLenum + GL_UNSIGNED_BYTE* = 0x1401.GLenum + GL_SHORT* = 0x1402.GLenum + GL_UNSIGNED_SHORT* = 0x1403.GLenum + GL_INT* = 0x1404.GLenum + GL_UNSIGNED_INT* = 0x1405.GLenum + GL_FLOAT* = 0x1406.GLenum + + GL_TEXTURE_2D* = 0x0DE1.GLenum + GL_TEXTURE_BUFFER* = 0x8C2A.GLenum + GL_TEXTURE0* = 0x84C0.GLenum + GL_TEXTURE1* = 0x84C1.GLenum + GL_TEXTURE_MAG_FILTER* = 0x2800.GLenum + GL_TEXTURE_MIN_FILTER* = 0x2801.GLenum + GL_TEXTURE_WRAP_S* = 0x2802.GLenum + GL_TEXTURE_WRAP_T* = 0x2803.GLenum + + GL_NEAREST* = 0x2600.GLenum + GL_LINEAR* = 0x2601.GLenum + GL_NEAREST_MIPMAP_NEAREST* = 0x2700.GLenum + GL_LINEAR_MIPMAP_NEAREST* = 0x2701.GLenum + GL_NEAREST_MIPMAP_LINEAR* = 0x2702.GLenum + GL_LINEAR_MIPMAP_LINEAR* = 0x2703.GLenum + + GL_REPEAT* = 0x2901.GLenum + GL_CLAMP_TO_EDGE* = 0x812F.GLenum + GL_MIRRORED_REPEAT* = 0x8370.GLenum + + GL_RGBA* = 0x1908.GLenum + GL_RGBA8* = 0x8058.GLenum + GL_R8* = 0x8229.GLenum + + GL_FRAMEBUFFER* = 0x8D40.GLenum + GL_COLOR_ATTACHMENT0* = 0x8CE0.GLenum + GL_FRAMEBUFFER_COMPLETE* = 0x8CD5.GLenum + + GL_ARRAY_BUFFER* = 0x8892.GLenum + GL_ELEMENT_ARRAY_BUFFER* = 0x8893.GLenum + GL_UNIFORM_BUFFER* = 0x8A11.GLenum + + GL_STREAM_DRAW* = 0x88E0.GLenum + GL_STATIC_DRAW* = 0x88E4.GLenum + + GL_TRIANGLES* = 0x0004.GLenum + + GL_COLOR_BUFFER_BIT* = 0x4000.GLenum + GL_DEPTH_BUFFER_BIT* = 0x0100.GLenum + + GL_BLEND* = 0x0BE2.GLenum + GL_SRC_ALPHA* = 0x0302.GLenum + GL_ONE_MINUS_SRC_ALPHA* = 0x0303.GLenum + GL_ONE* = 1.GLenum + + GL_DEPTH_TEST* = 0x0B71.GLenum + GL_LEQUAL* = 0x0203.GLenum + + GL_INFO_LOG_LENGTH* = 0x8B84.GLenum + GL_COMPILE_STATUS* = 0x8B81.GLenum + GL_LINK_STATUS* = 0x8B82.GLenum + GL_ACTIVE_ATTRIBUTES* = 0x8B89.GLenum + GL_ACTIVE_UNIFORMS* = 0x8B86.GLenum + + GL_VERTEX_SHADER* = 0x8B31.GLenum + GL_FRAGMENT_SHADER* = 0x8B30.GLenum + GL_COMPUTE_SHADER* = 0x91B9.GLenum + + GL_CONTEXT_FLAGS* = 0x821E.GLenum + GL_CONTEXT_FLAG_DEBUG_BIT* = 0x00000002.GLenum + GL_DEBUG_SEVERITY_NOTIFICATION* = 0x826B.GLenum + GL_DEBUG_OUTPUT_SYNCHRONOUS* = 0x8242.GLenum + GL_DEBUG_OUTPUT* = 0x92E0.GLenum + + cGL_FLOAT* = GL_FLOAT + cGL_INT* = GL_INT + cGL_BYTE* = GL_BYTE + cGL_SHORT* = GL_SHORT + cGL_UNSIGNED_BYTE* = GL_UNSIGNED_BYTE + cGL_UNSIGNED_SHORT* = GL_UNSIGNED_SHORT + + var glCtx* {.importc: "glCtx", nodecl.}: WebGL2RenderingContext + + proc setWebGLContext*(ctx: WebGL2RenderingContext) = + glCtx = ctx + + proc newFloat32Array*(data: openArray[float32]): webgl.Float32Array + {.importjs: "new Float32Array(#)".} + proc newUint16Array*(data: openArray[uint16]): webgl.Uint16Array + {.importjs: "new Uint16Array(#)".} + proc newUint8Array*(data: openArray[uint8]): webgl.Uint8Array + {.importjs: "new Uint8Array(#)".} + + proc glCreateBuffer*(): GlBufferId {.importjs: "glCtx.createBuffer()".} + proc glCreateTexture*(): GlTextureId {.importjs: "glCtx.createTexture()".} + proc glCreateVertexArray*(): GlVertexArrayId + {.importjs: "glCtx.createVertexArray()".} + proc glCreateFramebuffer*(): GlFramebufferId + {.importjs: "glCtx.createFramebuffer()".} + + proc glCreateShader*(kind: GLenum): GlShaderId + {.importjs: "glCtx.createShader(#)".} + proc glShaderSource*(shader: GlShaderId; source: cstring) + {.importjs: "glCtx.shaderSource(#, #)".} + proc glCompileShader*(shader: GlShaderId) + {.importjs: "glCtx.compileShader(#)".} + proc glGetShaderInfoLog*(shader: GlShaderId): cstring + {.importjs: "glCtx.getShaderInfoLog(#)".} + proc glGetShaderParameter*(shader: GlShaderId; pname: GLenum): bool + {.importjs: "glCtx.getShaderParameter(#, #)".} + + proc glCreateProgram*(): GlProgramId {.importjs: "glCtx.createProgram()".} + proc glAttachShader*(program: GlProgramId; shader: GlShaderId) + {.importjs: "glCtx.attachShader(#, #)".} + proc glLinkProgram*(program: GlProgramId) + {.importjs: "glCtx.linkProgram(#)".} + proc glGetProgramInfoLog*(program: GlProgramId): cstring + {.importjs: "glCtx.getProgramInfoLog(#)".} + proc glGetProgramParameter*(program: GlProgramId; pname: GLenum): int + {.importjs: "glCtx.getProgramParameter(#, #)".} + + proc glUseProgram*(program: GlProgramId) + {.importjs: "glCtx.useProgram(#)".} + + proc glGetAttribLocation*(program: GlProgramId; name: cstring): GLint + {.importjs: "glCtx.getAttribLocation(#, #)".} + proc glGetUniformLocation*(program: GlProgramId; + name: cstring): GlUniformLocation + {.importjs: "glCtx.getUniformLocation(#, #)".} + + proc glGetActiveAttrib*(program: GlProgramId; + index: GLuint): webgl.WebGLActiveInfo + {.importjs: "glCtx.getActiveAttrib(#, #)".} + proc glGetActiveUniform*(program: GlProgramId; + index: GLuint): webgl.WebGLActiveInfo + {.importjs: "glCtx.getActiveUniform(#, #)".} + + proc glUniform1i*(location: GlUniformLocation; v0: GLint) + {.importjs: "glCtx.uniform1i(#, #)".} + proc glUniform2i*(location: GlUniformLocation; v0, v1: GLint) + {.importjs: "glCtx.uniform2i(#, #, #)".} + proc glUniform3i*(location: GlUniformLocation; v0, v1, v2: GLint) + {.importjs: "glCtx.uniform3i(#, #, #, #)".} + proc glUniform4i*(location: GlUniformLocation; v0, v1, v2, v3: GLint) + {.importjs: "glCtx.uniform4i(#, #, #, #, #)".} + + proc glUniform1f*(location: GlUniformLocation; v0: GLfloat) + {.importjs: "glCtx.uniform1f(#, #)".} + proc glUniform2f*(location: GlUniformLocation; v0, v1: GLfloat) + {.importjs: "glCtx.uniform2f(#, #, #)".} + proc glUniform3f*(location: GlUniformLocation; v0, v1, v2: GLfloat) + {.importjs: "glCtx.uniform3f(#, #, #, #)".} + proc glUniform4f*(location: GlUniformLocation; v0, v1, v2, v3: GLfloat) + {.importjs: "glCtx.uniform4f(#, #, #, #, #)".} + + proc glUniformMatrix4fvRaw*(location: GlUniformLocation; transpose: GLboolean; + value: webgl.Float32Array) {.importjs: "glCtx.uniformMatrix4fv(#, #, #)".} + + proc glUniformMatrix4fv*( + location: GlUniformLocation; count: GLsizei; transpose: GLboolean; + value: openArray[float32]; + ) = + if count <= 0: + return + glUniformMatrix4fvRaw(location, transpose, newFloat32Array(value)) + + proc glBindBuffer*(target: GLenum; buffer: GlBufferId) + {.importjs: "glCtx.bindBuffer(#, #)".} + + proc glBufferData*(target: GLenum; size: int; usage: GLenum) + {.importjs: "glCtx.bufferData(#, #, #)".} + proc glBufferData*(target: GLenum; data: webgl.Float32Array; usage: GLenum) + {.importjs: "glCtx.bufferData(#, #, #)".} + proc glBufferData*(target: GLenum; data: webgl.Uint16Array; usage: GLenum) + {.importjs: "glCtx.bufferData(#, #, #)".} + proc glBufferData*(target: GLenum; data: webgl.Uint8Array; usage: GLenum) + {.importjs: "glCtx.bufferData(#, #, #)".} + + proc glBufferSubData*(target: GLenum; offset: GLintptr; + data: webgl.Float32Array) + {.importjs: "glCtx.bufferSubData(#, #, #)".} + proc glBufferSubData*(target: GLenum; offset: GLintptr; + data: webgl.Uint16Array) + {.importjs: "glCtx.bufferSubData(#, #, #)".} + proc glBufferSubData*(target: GLenum; offset: GLintptr; + data: webgl.Uint8Array) + {.importjs: "glCtx.bufferSubData(#, #, #)".} + + proc glBindTexture*(target: GLenum; texture: GlTextureId) + {.importjs: "glCtx.bindTexture(#, #)".} + proc glActiveTexture*(texture: GLenum) + {.importjs: "glCtx.activeTexture(#)".} + + proc glTexImage2D*( + target: GLenum; + level: GLint; + internalFormat: GLint; + width: GLsizei; + height: GLsizei; + border: GLint; + format: GLenum; + typ: GLenum; + data: JsObject; + ) {.importjs: "glCtx.texImage2D(#, #, #, #, #, #, #, #, #)".} + + proc glTexSubImage2D*( + target: GLenum; + level: GLint; + xoffset: GLint; + yoffset: GLint; + width: GLsizei; + height: GLsizei; + format: GLenum; + typ: GLenum; + data: JsObject; + ) {.importjs: "glCtx.texSubImage2D(#, #, #, #, #, #, #, #, #)".} + + proc glTexParameteri*(target: GLenum; pname: GLenum; param: GLint) + {.importjs: "glCtx.texParameteri(#, #, #)".} + proc glGenerateMipmap*(target: GLenum) + {.importjs: "glCtx.generateMipmap(#)".} + + proc glBindFramebuffer*(target: GLenum; framebuffer: GlFramebufferId) + {.importjs: "glCtx.bindFramebuffer(#, #)".} + proc glFramebufferTexture2D*( + target: GLenum; + attachment: GLenum; + textarget: GLenum; + texture: GlTextureId; + level: GLint; + ) {.importjs: "glCtx.framebufferTexture2D(#, #, #, #, #)".} + proc glCheckFramebufferStatus*(target: GLenum): GLenum + {.importjs: "glCtx.checkFramebufferStatus(#)".} + + proc glBindVertexArray*(vao: GlVertexArrayId) + {.importjs: "glCtx.bindVertexArray(#)".} + + proc glVertexAttribPointer*( + index: GLuint; + size: GLint; + typ: GLenum; + normalized: GLboolean; + stride: GLsizei; + offset: GLintptr; + ) {.importjs: "glCtx.vertexAttribPointer(#, #, #, #, #, #)".} + + proc glVertexAttribIPointer*( + index: GLuint; + size: GLint; + typ: GLenum; + stride: GLsizei; + offset: GLintptr; + ) {.importjs: "glCtx.vertexAttribIPointer(#, #, #, #, #)".} + + proc glEnableVertexAttribArray*(index: GLuint) + {.importjs: "glCtx.enableVertexAttribArray(#)".} + + proc glDrawElements*(mode: GLenum; count: GLsizei; typ: GLenum; + offset: GLintptr) + {.importjs: "glCtx.drawElements(#, #, #, #)".} + + proc glViewport*(x, y: GLint; width, height: GLsizei) + {.importjs: "glCtx.viewport(#, #, #, #)".} + proc glClearColor*(r, g, b, a: GLclampf) + {.importjs: "glCtx.clearColor(#, #, #, #)".} + proc glClear*(mask: GLbitfield) + {.importjs: "glCtx.clear(#)".} + + proc glEnable*(cap: GLenum) {.importjs: "glCtx.enable(#)".} + proc glDisable*(cap: GLenum) {.importjs: "glCtx.disable(#)".} + proc glBlendFunc*(sfactor, dfactor: GLenum) + {.importjs: "glCtx.blendFunc(#, #)".} + proc glBlendFuncSeparate*(srcRGB, dstRGB, srcAlpha, dstAlpha: GLenum) + {.importjs: "glCtx.blendFuncSeparate(#, #, #, #)".} + proc glDepthMask*(flag: GLboolean) + {.importjs: "glCtx.depthMask(#)".} + proc glDepthFunc*(mode: GLenum) + {.importjs: "glCtx.depthFunc(#)".} + + proc glGetError*(): GLenum {.importjs: "glCtx.getError()".} + proc glGetInteger*(pname: GLenum): GLint + {.importjs: "glCtx.getParameter(#)".} + proc glGetString*(pname: GLenum): cstring + {.importjs: "glCtx.getParameter(#)".} + + proc glBindBufferBase*(target: GLenum; index: GLuint; buffer: GlBufferId) + {.importjs: "glCtx.bindBufferBase(#, #, #)".} + proc glGetUniformBlockIndex*(program: GlProgramId; name: cstring): GLuint + {.importjs: "glCtx.getUniformBlockIndex(#, #)".} + proc glUniformBlockBinding*(program: GlProgramId; index: GLuint; + binding: GLuint) + {.importjs: "glCtx.uniformBlockBinding(#, #, #)".} + +else: + import pkg/opengl as opengl + export opengl + + type + GlBufferId* = GLuint + GlTextureId* = GLuint + GlProgramId* = GLuint + GlShaderId* = GLuint + GlVertexArrayId* = GLuint + GlFramebufferId* = GLuint + GlUniformLocation* = GLint diff --git a/src/figdraw/opengl/glcontext.nim b/src/figdraw/opengl/glcontext.nim index a6f02cf..4c95f4f 100644 --- a/src/figdraw/opengl/glcontext.nim +++ b/src/figdraw/opengl/glcontext.nim @@ -1,21 +1,28 @@ import - buffers, chroma, pixie, hashes, opengl, os, shaders, strformat, strutils, tables, + buffers, chroma, hashes, glapi, os, shaders, strformat, strutils, tables, textures, times +when not defined(js): + import pixie + import pixie/simd +else: + type Image* = object ## Copied from Fidget backend, copyright from @treeform applies -import pixie/simd - -import pkg/chronicles +import ../utils/logging import ../commons -import ../common/formatflippy +when not defined(js): + import ../common/formatflippy +else: + type Flippy* = object import ../fignodes -import ../utils/drawextras -import ../utils/drawboxes -import ../utils/drawshadows +when not defined(js): + import ../utils/drawextras + import ../utils/drawboxes + import ../utils/drawshadows -export drawextras + export drawextras logScope: scope = "opengl" @@ -25,7 +32,7 @@ proc round*(v: Vec2): Vec2 = const quadLimit = 10_921 -when defined(emscripten): +when defined(emscripten) or defined(js): type SdfModeData = float32 else: type SdfModeData = uint16 @@ -33,22 +40,23 @@ else: type Context* = ref object mainShader, maskShader, activeShader: Shader atlasTexture: Texture - maskTextureWrite: int ## Index into max textures for writing. - maskTextures: seq[Texture] ## Masks array for pushing and popping. - atlasSize: int ## Size x size dimensions of the atlas - atlasMargin: int ## Default margin between images - quadCount: int ## Number of quads drawn so far - maxQuads: int ## Max quads to draw before issuing an OpenGL call - mat*: Mat4 ## Current matrix - mats: seq[Mat4] ## Matrix stack + maskTextureWrite: int ## Index into max textures for writing. + maskTextures: seq[Texture] ## Masks array for pushing and popping. + atlasSize: int ## Size x size dimensions of the atlas + atlasMargin: int ## Default margin between images + quadCount: int ## Number of quads drawn so far + maxQuads: int ## Max quads to draw before issuing an OpenGL call + mat*: Mat4 ## Current matrix + mats: seq[Mat4] ## Matrix stack entries*: Table[Hash, Rect] ## Mapping of image name to atlas UV position - heights: seq[uint16] ## Height map of the free space in the atlas + heights: seq[uint16] ## Height map of the free space in the atlas proj*: Mat4 - frameSize: Vec2 ## Dimensions of the window frame - vertexArrayId, maskFramebufferId: GLuint + frameSize: Vec2 ## Dimensions of the window frame + vertexArrayId: GlVertexArrayId + maskFramebufferId: GlFramebufferId frameBegun, maskBegun: bool - pixelate*: bool ## Makes texture look pixelated, like a pixel game. - pixelScale*: float32 ## Multiple scaling factor. + pixelate*: bool ## Makes texture look pixelated, like a pixel game. + pixelScale*: float32 ## Multiple scaling factor. # Buffer data for OpenGL indices: tuple[buffer: Buffer, data: seq[uint16]] @@ -79,17 +87,28 @@ proc upload(ctx: Context) = ctx.colors.buffer.count = ctx.quadCount * 4 ctx.uvs.buffer.count = ctx.quadCount * 4 ctx.indices.buffer.count = ctx.quadCount * 6 - bindBufferData(ctx.positions.buffer.addr, ctx.positions.data[0].addr) - bindBufferData(ctx.colors.buffer.addr, ctx.colors.data[0].addr) - bindBufferData(ctx.uvs.buffer.addr, ctx.uvs.data[0].addr) + when defined(js): + bindBufferData(ctx.positions.buffer.addr, ctx.positions.data) + bindBufferData(ctx.colors.buffer.addr, ctx.colors.data) + bindBufferData(ctx.uvs.buffer.addr, ctx.uvs.data) + else: + bindBufferData(ctx.positions.buffer.addr, ctx.positions.data[0].addr) + bindBufferData(ctx.colors.buffer.addr, ctx.colors.data[0].addr) + bindBufferData(ctx.uvs.buffer.addr, ctx.uvs.data[0].addr) ctx.sdfParams.buffer.count = ctx.quadCount * 4 ctx.sdfRadii.buffer.count = ctx.quadCount * 4 ctx.sdfModeAttr.buffer.count = ctx.quadCount * 4 ctx.sdfFactors.buffer.count = ctx.quadCount * 4 - bindBufferData(ctx.sdfParams.buffer.addr, ctx.sdfParams.data[0].addr) - bindBufferData(ctx.sdfRadii.buffer.addr, ctx.sdfRadii.data[0].addr) - bindBufferData(ctx.sdfModeAttr.buffer.addr, ctx.sdfModeAttr.data[0].addr) - bindBufferData(ctx.sdfFactors.buffer.addr, ctx.sdfFactors.data[0].addr) + when defined(js): + bindBufferData(ctx.sdfParams.buffer.addr, ctx.sdfParams.data) + bindBufferData(ctx.sdfRadii.buffer.addr, ctx.sdfRadii.data) + bindBufferData(ctx.sdfModeAttr.buffer.addr, ctx.sdfModeAttr.data) + bindBufferData(ctx.sdfFactors.buffer.addr, ctx.sdfFactors.data) + else: + bindBufferData(ctx.sdfParams.buffer.addr, ctx.sdfParams.data[0].addr) + bindBufferData(ctx.sdfRadii.buffer.addr, ctx.sdfRadii.data[0].addr) + bindBufferData(ctx.sdfModeAttr.buffer.addr, ctx.sdfModeAttr.data[0].addr) + bindBufferData(ctx.sdfFactors.buffer.addr, ctx.sdfFactors.data[0].addr) proc setUpMaskFramebuffer(ctx: Context) = glBindFramebuffer(GL_FRAMEBUFFER, ctx.maskFramebufferId) @@ -102,8 +121,12 @@ proc setUpMaskFramebuffer(ctx: Context) = ) proc createAtlasTexture(ctx: Context, size: int): Texture = - result.width = size.GLint - result.height = size.GLint + when defined(js): + result.width = int32(size) + result.height = int32(size) + else: + result.width = size.GLint + result.height = size.GLint result.componentType = GL_UNSIGNED_BYTE result.format = GL_RGBA result.internalFormat = GL_RGBA8 @@ -123,7 +146,7 @@ proc addMaskTexture(ctx: Context, frameSize = vec2(1, 1)) = maskTexture.height = frameSize.y.int32 maskTexture.componentType = GL_UNSIGNED_BYTE maskTexture.format = GL_RGBA - when defined(emscripten): + when defined(emscripten) or defined(js): maskTexture.internalFormat = GL_RGBA8 else: maskTexture.internalFormat = GL_R8 @@ -161,7 +184,12 @@ proc newContext*( result.addMaskTexture() - when defined(emscripten) or defined(useOpenGlEs): + when defined(js): + result.maskShader = + newShaderStatic("glsl/webgl2/atlas.vert", "glsl/webgl2/mask.frag") + result.mainShader = + newShaderStatic("glsl/webgl2/atlas.vert", "glsl/webgl2/atlas.frag") + elif defined(emscripten) or defined(useOpenGlEs): result.maskShader = newShaderStatic("glsl/emscripten/atlas.vert", "glsl/emscripten/mask.frag") result.mainShader = @@ -213,7 +241,7 @@ proc newContext*( result.sdfRadii.data = newSeq[float32](result.sdfRadii.buffer.kind.componentCount() * maxQuads * 4) - when defined(emscripten): + when defined(emscripten) or defined(js): result.sdfModeAttr.buffer.componentType = cGL_FLOAT else: result.sdfModeAttr.buffer.componentType = GL_UNSIGNED_SHORT @@ -221,7 +249,8 @@ proc newContext*( result.sdfModeAttr.buffer.target = GL_ARRAY_BUFFER result.sdfModeAttr.buffer.usage = GL_STREAM_DRAW result.sdfModeAttr.data = - newSeq[SdfModeData](result.sdfModeAttr.buffer.kind.componentCount() * maxQuads * 4) + newSeq[SdfModeData](result.sdfModeAttr.buffer.kind.componentCount() * + maxQuads * 4) result.sdfFactors.buffer.componentType = cGL_FLOAT result.sdfFactors.buffer.kind = bkVEC2 @@ -250,14 +279,21 @@ proc newContext*( ) # Indices are only uploaded once - bindBufferData(result.indices.buffer.addr, result.indices.data[0].addr) + when defined(js): + bindBufferData(result.indices.buffer.addr, result.indices.data) + else: + bindBufferData(result.indices.buffer.addr, result.indices.data[0].addr) result.upload() result.activeShader = result.mainShader - glGenVertexArrays(1, result.vertexArrayId.addr) - glBindVertexArray(result.vertexArrayId) + when defined(js): + result.vertexArrayId = glCreateVertexArray() + glBindVertexArray(result.vertexArrayId) + else: + glGenVertexArrays(1, result.vertexArrayId.addr) + glBindVertexArray(result.vertexArrayId) # Main shader (atlas + SDF). result.mainShader.bindAttrib("vertexPos", result.positions.buffer) @@ -277,14 +313,20 @@ proc newContext*( result.maskShader.bindAttrib("vertexSdfMode", result.sdfModeAttr.buffer) # Create mask framebuffer - glGenFramebuffers(1, result.maskFramebufferId.addr) + when defined(js): + result.maskFramebufferId = glCreateFramebuffer() + else: + glGenFramebuffers(1, result.maskFramebufferId.addr) result.setUpMaskFramebuffer() let status = glCheckFramebufferStatus(GL_FRAMEBUFFER) if status != GL_FRAMEBUFFER_COMPLETE: quit(&"Something wrong with mask framebuffer: {toHex(status.int32, 4)}") - glBindFramebuffer(GL_FRAMEBUFFER, 0) + when defined(js): + glBindFramebuffer(GL_FRAMEBUFFER, nil) + else: + glBindFramebuffer(GL_FRAMEBUFFER, 0) func `[]=`(t: var Table[Hash, Rect], key: string, rect: Rect) = t[hash(key)] = rect @@ -347,53 +389,73 @@ proc findEmptyRect(ctx: Context, width, height: int): Rect = return rect -proc putImage*(ctx: Context, path: Hash, image: Image) = - # Reminder: This does not set mipmaps (used for text, should it?) - let rect = ctx.findEmptyRect(image.width, image.height) - ctx.entries[path] = rect / float(ctx.atlasSize) - updateSubImage(ctx.atlasTexture, int(rect.x), int(rect.y), image) - -proc addImage*(ctx: Context, key: Hash, image: Image) = - ctx.putImage(key, image) - -proc updateImage*(ctx: Context, path: Hash, image: Image) = - ## Updates an image that was put there with putImage. - ## Useful for things like video. - ## * Must be the same size. - ## * This does not set mipmaps. - let rect = ctx.entries[path] - assert rect.w == image.width.float / float(ctx.atlasSize) - assert rect.h == image.height.float / float(ctx.atlasSize) - updateSubImage( - ctx.atlasTexture, - int(rect.x * ctx.atlasSize.float), - int(rect.y * ctx.atlasSize.float), - image, - ) +when not defined(js): + proc putImage*(ctx: Context, path: Hash, image: Image) = + # Reminder: This does not set mipmaps (used for text, should it?) + let rect = ctx.findEmptyRect(image.width, image.height) + ctx.entries[path] = rect / float(ctx.atlasSize) + updateSubImage(ctx.atlasTexture, int(rect.x), int(rect.y), image) + + proc addImage*(ctx: Context, key: Hash, image: Image) = + ctx.putImage(key, image) + + proc updateImage*(ctx: Context, path: Hash, image: Image) = + ## Updates an image that was put there with putImage. + ## Useful for things like video. + ## * Must be the same size. + ## * This does not set mipmaps. + let rect = ctx.entries[path] + assert rect.w == image.width.float / float(ctx.atlasSize) + assert rect.h == image.height.float / float(ctx.atlasSize) + updateSubImage( + ctx.atlasTexture, + int(rect.x * ctx.atlasSize.float), + int(rect.y * ctx.atlasSize.float), + image, + ) +else: + proc putImage*(ctx: Context, path: Hash, image: Image) = + discard + + proc addImage*(ctx: Context, key: Hash, image: Image) = + discard + + proc updateImage*(ctx: Context, path: Hash, image: Image) = + discard + +when not defined(js): + proc logFlippy(flippy: Flippy, file: string) = + debug "putFlippy file", + fwidth = $flippy.width, fheight = $flippy.height, flippyPath = file + + proc putFlippy*(ctx: Context, path: Hash, flippy: Flippy) = + logFlippy(flippy, $path) + let rect = ctx.findEmptyRect(flippy.width, flippy.height) + ctx.entries[path] = rect / float(ctx.atlasSize) + var + x = int(rect.x) + y = int(rect.y) + for level, mip in flippy.mipmaps: + updateSubImage(ctx.atlasTexture, x, y, mip, level) + x = x div 2 + y = y div 2 + + proc putImage*(ctx: Context, imgObj: ImgObj) = + ## puts an ImgObj wrapper with either a flippy or image format + case imgObj.kind: + of FlippyImg: + ctx.putFlippy(imgObj.id.Hash, imgObj.flippy) + of PixieImg: + ctx.putImage(imgObj.id.Hash, imgObj.pimg) +else: + proc logFlippy(flippy: Flippy, file: string) = + discard -proc logFlippy(flippy: Flippy, file: string) = - debug "putFlippy file", - fwidth = $flippy.width, fheight = $flippy.height, flippyPath = file - -proc putFlippy*(ctx: Context, path: Hash, flippy: Flippy) = - logFlippy(flippy, $path) - let rect = ctx.findEmptyRect(flippy.width, flippy.height) - ctx.entries[path] = rect / float(ctx.atlasSize) - var - x = int(rect.x) - y = int(rect.y) - for level, mip in flippy.mipmaps: - updateSubImage(ctx.atlasTexture, x, y, mip, level) - x = x div 2 - y = y div 2 - -proc putImage*(ctx: Context, imgObj: ImgObj) = - ## puts an ImgObj wrapper with either a flippy or image format - case imgObj.kind: - of FlippyImg: - ctx.putFlippy(imgObj.id.Hash, imgObj.flippy) - of PixieImg: - ctx.putImage(imgObj.id.Hash, imgObj.pimg) + proc putFlippy*(ctx: Context, path: Hash, flippy: Flippy) = + discard + + proc putImage*(ctx: Context, imgObj: ImgObj) = + discard proc flush(ctx: Context, maskTextureRead: int = ctx.maskTextureWrite) = ## Flips - draws current buffer and starts a new one. @@ -429,9 +491,20 @@ proc flush(ctx: Context, maskTextureRead: int = ctx.maskTextureWrite) = ctx.activeShader.bindUniforms() glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ctx.indices.buffer.bufferId) - glDrawElements( - GL_TRIANGLES, ctx.indices.buffer.count.GLint, ctx.indices.buffer.componentType, nil - ) + when defined(js): + glDrawElements( + GL_TRIANGLES, + int32(ctx.indices.buffer.count), + ctx.indices.buffer.componentType, + 0.GLintptr, + ) + else: + glDrawElements( + GL_TRIANGLES, + ctx.indices.buffer.count.GLint, + ctx.indices.buffer.componentType, + nil, + ) ctx.quadCount = 0 @@ -504,7 +577,7 @@ proc drawQuad*( ctx.sdfFactors.data.setVert2(offset + 3, defaultFactors) # atlas fragment mode - when defined(emscripten): + when defined(emscripten) or defined(js): let modeVal = 0.0'f32 else: let modeVal = 0'u16 @@ -584,7 +657,7 @@ proc drawUvRect(ctx: Context, at, to: Vec2, uvAt, uvTo: Vec2, color: Color) = ctx.sdfFactors.data.setVert2(offset + 2, defaultFactors) ctx.sdfFactors.data.setVert2(offset + 3, defaultFactors) - when defined(emscripten): + when defined(emscripten) or defined(js): let modeVal = 0.0'f32 else: let modeVal = 0'u16 @@ -601,84 +674,155 @@ proc drawUvRect(ctx: Context, rect, uvRect: Rect, color: Color) = proc getImageRect(ctx: Context, imageId: Hash): Rect = return ctx.entries[imageId] -proc drawImage*( - ctx: Context, - imageId: Hash, - pos: Vec2 = vec2(0, 0), - color = color(1, 1, 1, 1), - scale = 1.0, -) = - ## Draws image the UI way - pos at top-left. - let - rect = ctx.getImageRect(imageId) - wh = rect.wh * ctx.atlasSize.float32 * scale - ctx.drawUvRect(pos, pos + wh, rect.xy, rect.xy + rect.wh, color) - -proc drawImage*( - ctx: Context, - imageId: Hash, - pos: Vec2 = vec2(0, 0), - color = color(1, 1, 1, 1), - size: Vec2, -) = - ## Draws image the UI way - pos at top-left. - let rect = ctx.getImageRect(imageId) - ctx.drawUvRect(pos, pos + size, rect.xy, rect.xy + rect.wh, color) - -proc drawImageAdj*( - ctx: Context, - imageId: Hash, - pos: Vec2 = vec2(0, 0), - color = color(1, 1, 1, 1), - size: Vec2, -) = - ## Draws image the UI way - pos at top-left. - let - rect = ctx.getImageRect(imageId) - adj = vec2(2 / ctx.atlasSize.float32) - ctx.drawUvRect(pos, pos + size, rect.xy + adj, rect.xy + rect.wh - adj, color) - -proc drawSprite*( - ctx: Context, - imageId: Hash, - pos: Vec2 = vec2(0, 0), - color = color(1, 1, 1, 1), - scale = 1.0, -) = - ## Draws image the game way - pos at center. - let - rect = ctx.getImageRect(imageId) - wh = rect.wh * ctx.atlasSize.float32 * scale - ctx.drawUvRect(pos - wh / 2, pos + wh / 2, rect.xy, rect.xy + rect.wh, color) +when not defined(js): + proc drawImage*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + scale = 1.0, + ) = + ## Draws image the UI way - pos at top-left. + let + rect = ctx.getImageRect(imageId) + wh = rect.wh * ctx.atlasSize.float32 * scale + ctx.drawUvRect(pos, pos + wh, rect.xy, rect.xy + rect.wh, color) +else: + proc drawImage*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + scale = 1.0, + ) = + discard + +when not defined(js): + proc drawImage*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + size: Vec2, + ) = + ## Draws image the UI way - pos at top-left. + let rect = ctx.getImageRect(imageId) + ctx.drawUvRect(pos, pos + size, rect.xy, rect.xy + rect.wh, color) +else: + proc drawImage*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + size: Vec2, + ) = + discard + +when not defined(js): + proc drawImageAdj*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + size: Vec2, + ) = + ## Draws image the UI way - pos at top-left. + let + rect = ctx.getImageRect(imageId) + adj = vec2(2 / ctx.atlasSize.float32) + ctx.drawUvRect(pos, pos + size, rect.xy + adj, rect.xy + rect.wh - adj, color) +else: + proc drawImageAdj*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + size: Vec2, + ) = + discard + +when not defined(js): + proc drawSprite*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + scale = 1.0, + ) = + ## Draws image the game way - pos at center. + let + rect = ctx.getImageRect(imageId) + wh = rect.wh * ctx.atlasSize.float32 * scale + ctx.drawUvRect(pos - wh / 2, pos + wh / 2, rect.xy, rect.xy + rect.wh, color) +else: + proc drawSprite*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + scale = 1.0, + ) = + discard + +when not defined(js): + proc drawSprite*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + size: Vec2, + ) = + ## Draws image the game way - pos at center. + let rect = ctx.getImageRect(imageId) + ctx.drawUvRect( + pos - size / 2, pos + size / 2, rect.xy, rect.xy + rect.wh, color + ) +else: + proc drawSprite*( + ctx: Context, + imageId: Hash, + pos: Vec2 = vec2(0, 0), + color = color(1, 1, 1, 1), + size: Vec2, + ) = + discard -proc drawSprite*( +proc drawRoundedRectSdf*( ctx: Context, - imageId: Hash, - pos: Vec2 = vec2(0, 0), - color = color(1, 1, 1, 1), - size: Vec2, -) = - ## Draws image the game way - pos at center. - let rect = ctx.getImageRect(imageId) - ctx.drawUvRect(pos - size / 2, pos + size / 2, rect.xy, rect.xy + rect.wh, color) + rect: Rect, + color: Color, + radii: array[DirectionCorners, float32], + mode: SdfMode = sdfModeClipAA, + factor: float32 = 4.0, + spread: float32 = 0.0, + shapeSize: Vec2 = vec2(0.0'f32, 0.0'f32), +) -proc drawRect*(ctx: Context, rect: Rect, color: Color) = - const imgKey = hash("rect") - if imgKey notin ctx.entries: - var image = newImage(4, 4) - image.fill(rgba(255, 255, 255, 255)) - ctx.putImage(imgKey, image) +when not defined(js): + proc drawRect*(ctx: Context, rect: Rect, color: Color) = + const imgKey = hash("rect") + if imgKey notin ctx.entries: + var image = newImage(4, 4) + image.fill(rgba(255, 255, 255, 255)) + ctx.putImage(imgKey, image) - let - uvRect = ctx.entries[imgKey] - wh = rect.wh * float32(ctx.atlasSize) - ctx.drawUvRect( - rect.xy, - rect.xy + rect.wh, - uvRect.xy + uvRect.wh / 2, - uvRect.xy + uvRect.wh / 2, - color, - ) + let + uvRect = ctx.entries[imgKey] + wh = rect.wh * float32(ctx.atlasSize) + ctx.drawUvRect( + rect.xy, + rect.xy + rect.wh, + uvRect.xy + uvRect.wh / 2, + uvRect.xy + uvRect.wh / 2, + color, + ) +else: + proc drawRect*(ctx: Context, rect: Rect, color: Color) = + ctx.drawRoundedRectSdf( + rect = rect, + color = color, + radii = [0'f32, 0'f32, 0'f32, 0'f32], + ) proc drawRoundedRectSdf*( ctx: Context, @@ -699,28 +843,30 @@ proc drawRoundedRectSdf*( let quadHalfExtents = rect.wh * 0.5'f32 resolvedShapeSize = - (if shapeSize.x > 0.0'f32 and shapeSize.y > 0.0'f32: shapeSize else: rect.wh) + (if shapeSize.x > 0.0'f32 and shapeSize.y > + 0.0'f32: shapeSize else: rect.wh) shapeHalfExtents = resolvedShapeSize * 0.5'f32 params = - vec4(quadHalfExtents.x, quadHalfExtents.y, shapeHalfExtents.x, shapeHalfExtents.y) + vec4(quadHalfExtents.x, quadHalfExtents.y, shapeHalfExtents.x, + shapeHalfExtents.y) maxRadius = min(shapeHalfExtents.x, shapeHalfExtents.y) radiiClamped = [ dcTopLeft: ( if radii[dcTopLeft] <= 0.0'f32: 0.0'f32 - else: max(1.0'f32, min(radii[dcTopLeft], maxRadius)).round() - ), + else: max(1.0'f32, min(radii[dcTopLeft], maxRadius)).round() + ), dcTopRight: ( if radii[dcTopRight] <= 0.0'f32: 0.0'f32 - else: max(1.0'f32, min(radii[dcTopRight], maxRadius)).round() - ), + else: max(1.0'f32, min(radii[dcTopRight], maxRadius)).round() + ), dcBottomLeft: ( if radii[dcBottomLeft] <= 0.0'f32: 0.0'f32 - else: max(1.0'f32, min(radii[dcBottomLeft], maxRadius)).round() - ), + else: max(1.0'f32, min(radii[dcBottomLeft], maxRadius)).round() + ), dcBottomRight: ( if radii[dcBottomRight] <= 0.0'f32: 0.0'f32 - else: max(1.0'f32, min(radii[dcBottomRight], maxRadius)).round() - ), + else: max(1.0'f32, min(radii[dcBottomRight], maxRadius)).round() + ), ] # (top-right, bottom-right, top-left, bottom-left) r4 = vec4( @@ -784,7 +930,7 @@ proc drawRoundedRectSdf*( ctx.sdfFactors.data.setVert2(offset + 2, factors) ctx.sdfFactors.data.setVert2(offset + 3, factors) - when defined(emscripten): + when defined(emscripten) or defined(js): let modeVal = mode.int.float32 else: let modeVal = mode.int.uint16 @@ -795,33 +941,38 @@ proc drawRoundedRectSdf*( inc ctx.quadCount -proc line*(ctx: Context, a: Vec2, b: Vec2, weight: float32, color: Color) = - let hash = hash((2345, a, b, (weight * 100).int, hash(color))) - - let - w = ceil(abs(b.x - a.x)).int - h = ceil(abs(a.y - b.y)).int - pos = vec2(min(a.x, b.x), min(a.y, b.y)) - - if w == 0 or h == 0: - return +when not defined(js): + proc line*(ctx: Context, a: Vec2, b: Vec2, weight: float32, color: Color) = + let hash = hash((2345, a, b, (weight * 100).int, hash(color))) - if hash notin ctx.entries: let - image = newImage(w, h) - c = newContext(image) - c.fillStyle = rgba(255, 255, 255, 255) - c.lineWidth = weight - c.strokeSegment(segment(a - pos, b - pos)) - ctx.putImage(hash, image) - let - uvRect = ctx.entries[hash] - wh = vec2(w.float32, h.float32) * ctx.atlasSize.float32 - ctx.drawUvRect( - pos, pos + vec2(w.float32, h.float32), uvRect.xy, uvRect.xy + uvRect.wh, color - ) + w = ceil(abs(b.x - a.x)).int + h = ceil(abs(a.y - b.y)).int + pos = vec2(min(a.x, b.x), min(a.y, b.y)) + + if w == 0 or h == 0: + return + + if hash notin ctx.entries: + let + image = newImage(w, h) + c = newContext(image) + c.fillStyle = rgba(255, 255, 255, 255) + c.lineWidth = weight + c.strokeSegment(segment(a - pos, b - pos)) + ctx.putImage(hash, image) + let + uvRect = ctx.entries[hash] + wh = vec2(w.float32, h.float32) * ctx.atlasSize.float32 + ctx.drawUvRect( + pos, pos + vec2(w.float32, h.float32), uvRect.xy, uvRect.xy + uvRect.wh, color + ) +else: + proc line*(ctx: Context, a: Vec2, b: Vec2, weight: float32, color: Color) = + discard -proc linePolygon*(ctx: Context, poly: seq[Vec2], weight: float32, color: Color) = +proc linePolygon*(ctx: Context, poly: seq[Vec2], weight: float32, + color: Color) = for i in 0 ..< poly.len: ctx.line(poly[i], poly[(i + 1) mod poly.len], weight, color) @@ -836,7 +987,10 @@ proc clearMask*(ctx: Context) = glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) - glBindFramebuffer(GL_FRAMEBUFFER, 0) + when defined(js): + glBindFramebuffer(GL_FRAMEBUFFER, nil) + else: + glBindFramebuffer(GL_FRAMEBUFFER, 0) proc beginMask*(ctx: Context) = ## Starts drawing into a mask. @@ -851,7 +1005,10 @@ proc beginMask*(ctx: Context) = ctx.addMaskTexture(ctx.frameSize) ctx.setUpMaskFramebuffer() - glViewport(0, 0, ctx.frameSize.x.GLint, ctx.frameSize.y.GLint) + when defined(js): + glViewport(0, 0, int32(ctx.frameSize.x), int32(ctx.frameSize.y)) + else: + glViewport(0, 0, ctx.frameSize.x.GLint, ctx.frameSize.y.GLint) glClearColor(0, 0, 0, 0) glClear(GL_COLOR_BUFFER_BIT) @@ -865,7 +1022,10 @@ proc endMask*(ctx: Context) = ctx.flush(ctx.maskTextureWrite - 1) - glBindFramebuffer(GL_FRAMEBUFFER, 0) + when defined(js): + glBindFramebuffer(GL_FRAMEBUFFER, nil) + else: + glBindFramebuffer(GL_FRAMEBUFFER, 0) ctx.activeShader = ctx.mainShader @@ -892,7 +1052,10 @@ proc beginFrame*(ctx: Context, frameSize: Vec2, proj: Mat4) = # Never resize the 0th mask because its just white. bindTextureData(ctx.maskTextures[i].addr, nil) - glViewport(0, 0, ctx.frameSize.x.GLint, ctx.frameSize.y.GLint) + when defined(js): + glViewport(0, 0, int32(ctx.frameSize.x), int32(ctx.frameSize.y)) + else: + glViewport(0, 0, ctx.frameSize.x.GLint, ctx.frameSize.y.GLint) ctx.clearMask() diff --git a/src/figdraw/opengl/glsl/webgl2/atlas.frag b/src/figdraw/opengl/glsl/webgl2/atlas.frag new file mode 100644 index 0000000..a3703f7 --- /dev/null +++ b/src/figdraw/opengl/glsl/webgl2/atlas.frag @@ -0,0 +1,126 @@ +#version 300 es + +precision highp float; + +in vec2 pos; +in vec2 uv; +in vec4 color; +in vec4 sdfParams; +in vec4 sdfRadii; +in float sdfMode; +in vec2 sdfFactors; + +uniform vec2 windowFrame; +uniform sampler2D atlasTex; +uniform sampler2D maskTex; +uniform float aaFactor; +uniform bool maskTexEnabled; + +out vec4 fragColor; + +const int sdfModeAtlas = 0; +const int sdfModeClipAA = 3; +const int sdfModeDropShadow = 7; +const int sdfModeDropShadowAA = 8; +const int sdfModeInsetShadow = 9; +const int sdfModeInsetShadowAnnular = 10; +const int sdfModeAnnular = 11; +const int sdfModeAnnularAA = 12; + +float sdRoundedBox(vec2 p, vec2 b, vec4 r) { + float rr; + if (p.x > 0.0) { + if (p.y > 0.0) { + rr = r.x; + } else { + rr = r.y; + } + } else { + if (p.y > 0.0) { + rr = r.z; + } else { + rr = r.w; + } + } + + vec2 q = abs(p) - b + vec2(rr, rr); + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; +} + +float gaussian(float x, float s) { + return 1.0 / (s * sqrt(6.283185307179586)) * + exp(-1.0 * (x * x) / (2.0 * s * s)); +} + +void main() { + vec2 quadHalfExtents = sdfParams.xy; + vec2 shapeHalfExtents = sdfParams.zw; + + vec2 p = vec2( + (uv.x - 0.5) * 2.0 * quadHalfExtents.x, + (uv.y - 0.5) * 2.0 * quadHalfExtents.y + ); + + float dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); + + float sdfFactor = sdfFactors.x; + float sdfSpread = sdfFactors.y; + int sdfModeInt = int(sdfMode); + + float alpha = 0.0; + vec4 outColor; + if (sdfModeInt == sdfModeAtlas) { + vec4 tex = texture(atlasTex, uv); + outColor = vec4( + tex.x * color.x, + tex.y * color.y, + tex.z * color.z, + tex.w * color.w + ); + } else { + float stdDevFactor = 1.0 / 2.2; + if (sdfModeInt == sdfModeAnnular) { + float f = sdfFactor * 0.5; + float sd = abs(dist + f) - f; + alpha = (sd < 0.0) ? 1.0 : 0.0; + } else if (sdfModeInt == sdfModeAnnularAA) { + float f = sdfFactor * 0.5; + float sd = abs(dist + f) - f; + float cl = clamp(aaFactor * sd + 0.5, 0.0, 1.0); + alpha = 1.0 - cl; + } else if (sdfModeInt == sdfModeDropShadow) { + float sd = dist - sdfSpread + 1.0; + float x = sd / (sdfFactor + 0.5); + float a = 1.1 * gaussian(x, stdDevFactor); + alpha = (sd > 0.0) ? min(a, 1.0) : 1.0; + } else if (sdfModeInt == sdfModeDropShadowAA) { + float cl = clamp(aaFactor * dist + 0.5, 0.0, 1.0); + float insideAlpha = 1.0 - cl; + float sd = dist - sdfSpread + 1.0; + float x = sd / (sdfFactor + 0.5); + float a = 1.1 * gaussian(x, stdDevFactor); + alpha = (sd >= 0.0) ? min(a, 1.0) : insideAlpha; + } else if (sdfModeInt == sdfModeInsetShadow) { + float sd = dist + sdfSpread + 1.0; + float x = sd / (sdfFactor + 0.5); + float a = 1.1 * gaussian(x, stdDevFactor); + alpha = (sd < 0.0) ? min(a, 1.0) : 1.0; + } else if (sdfModeInt == sdfModeInsetShadowAnnular) { + float sd = dist + sdfSpread + 1.0; + float x = sd / (sdfFactor + 0.5); + float a = 1.1 * gaussian(x, stdDevFactor); + alpha = (sd < 0.0) ? min(a, 1.0) : 0.0; + } else { + float cl = clamp(aaFactor * dist + 0.5, 0.0, 1.0); + alpha = 1.0 - cl; + } + + outColor = vec4(color.x, color.y, color.z, color.w * alpha); + } + + vec2 normalizedPos = vec2(pos.x / windowFrame.x, 1.0 - pos.y / windowFrame.y); + if (maskTexEnabled) { + outColor.a *= texture(maskTex, normalizedPos).r; + } + fragColor = outColor; +} diff --git a/src/figdraw/opengl/glsl/webgl2/atlas.vert b/src/figdraw/opengl/glsl/webgl2/atlas.vert new file mode 100644 index 0000000..655e1ca --- /dev/null +++ b/src/figdraw/opengl/glsl/webgl2/atlas.vert @@ -0,0 +1,32 @@ +#version 300 es + +precision highp float; + +in vec2 vertexPos; +in vec2 vertexUv; +in vec4 vertexColor; +in vec4 vertexSdfParams; +in vec4 vertexSdfRadii; +in float vertexSdfMode; +in vec2 vertexSdfFactors; + +uniform mat4 proj; + +out vec2 pos; +out vec2 uv; +out vec4 color; +out vec4 sdfParams; +out vec4 sdfRadii; +out float sdfMode; +out vec2 sdfFactors; + +void main() { + pos = vertexPos; + uv = vertexUv; + color = vertexColor; + sdfParams = vertexSdfParams; + sdfRadii = vertexSdfRadii; + sdfMode = vertexSdfMode; + sdfFactors = vertexSdfFactors; + gl_Position = proj * vec4(vertexPos.x, vertexPos.y, 0.0, 1.0); +} diff --git a/src/figdraw/opengl/glsl/webgl2/mask.frag b/src/figdraw/opengl/glsl/webgl2/mask.frag new file mode 100644 index 0000000..a6971d5 --- /dev/null +++ b/src/figdraw/opengl/glsl/webgl2/mask.frag @@ -0,0 +1,64 @@ +#version 300 es + +precision highp float; + +in vec2 pos; +in vec2 uv; +in vec4 color; +in vec4 sdfParams; +in vec4 sdfRadii; +in float sdfMode; + +uniform vec2 windowFrame; +uniform sampler2D atlasTex; +uniform sampler2D maskTex; +uniform float aaFactor; +uniform bool maskTexEnabled; + +out vec4 fragColor; + +const int sdfModeAtlas = 0; + +float sdRoundedBox(vec2 p, vec2 b, vec4 r) { + float rr; + if (p.x > 0.0) { + if (p.y > 0.0) { + rr = r.x; + } else { + rr = r.y; + } + } else { + if (p.y > 0.0) { + rr = r.z; + } else { + rr = r.w; + } + } + + vec2 q = abs(p) - b + vec2(rr, rr); + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; +} + +void main() { + float alpha; + int sdfModeInt = int(sdfMode); + if (sdfModeInt == sdfModeAtlas) { + alpha = texture(atlasTex, uv).a * color.a; + } else { + vec2 quadHalfExtents = sdfParams.xy; + vec2 shapeHalfExtents = sdfParams.zw; + vec2 p = vec2( + (uv.x - 0.5) * 2.0 * quadHalfExtents.x, + (uv.y - 0.5) * 2.0 * quadHalfExtents.y + ); + float dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); + float cl = clamp(aaFactor * dist + 0.5, 0.0, 1.0); + alpha = (1.0 - cl) * color.a; + } + + vec2 normalizedPos = vec2(pos.x / windowFrame.x, 1.0 - pos.y / windowFrame.y); + if (maskTexEnabled) { + alpha *= texture(maskTex, normalizedPos).r; + } + fragColor = vec4(alpha); +} diff --git a/src/figdraw/opengl/renderer.nim b/src/figdraw/opengl/renderer.nim index 8fbd555..2b6906b 100644 --- a/src/figdraw/opengl/renderer.nim +++ b/src/figdraw/opengl/renderer.nim @@ -1,15 +1,19 @@ import std/[hashes, math, tables, unicode] export tables -from pixie import Image, newImage, flipVertical +when not defined(js): + from pixie import Image, newImage, flipVertical +else: + type Image* = object import pkg/chroma -import pkg/chronicles -import pkg/opengl +import ../utils/logging +import glapi import ../commons import ../utils/glutils -import ../utils/drawshadows -import ../utils/drawboxes +when not defined(js): + import ../utils/drawshadows + import ../utils/drawboxes import glcommons, glcontext const FastShadows {.booldefine: "figuro.fastShadows".}: bool = false @@ -17,32 +21,39 @@ const FastShadows {.booldefine: "figuro.fastShadows".}: bool = false type OpenGLRenderer* = ref object ctx*: Context -proc takeScreenshot*(frame: Rect = rect(0, 0, 0, 0), readFront: bool = true): Image = - var viewport: array[4, GLint] - glGetIntegerv(GL_VIEWPORT, viewport[0].addr) - - let - viewportWidth = viewport[2].int - viewportHeight = viewport[3].int - - var x = frame.x.int - var y = frame.y.int - var w = frame.w.int - var h = frame.h.int - - if w <= 0 or h <= 0: - x = 0 - y = 0 - w = viewportWidth - h = viewportHeight - - glReadBuffer(if readFront: GL_FRONT else: GL_BACK) - result = newImage(w, h) - glReadPixels( - x.GLint, y.GLint, w.GLint, h.GLint, GL_RGBA, GL_UNSIGNED_BYTE, result.data[0].addr - ) - result.flipVertical() - glReadBuffer(GL_BACK) +when not defined(js): + proc takeScreenshot*(frame: Rect = rect(0, 0, 0, 0), + readFront: bool = true): Image = + var viewport: array[4, GLint] + glGetIntegerv(GL_VIEWPORT, viewport[0].addr) + + let + viewportWidth = viewport[2].int + viewportHeight = viewport[3].int + + var x = frame.x.int + var y = frame.y.int + var w = frame.w.int + var h = frame.h.int + + if w <= 0 or h <= 0: + x = 0 + y = 0 + w = viewportWidth + h = viewportHeight + + glReadBuffer(if readFront: GL_FRONT else: GL_BACK) + result = newImage(w, h) + glReadPixels( + x.GLint, y.GLint, w.GLint, h.GLint, GL_RGBA, GL_UNSIGNED_BYTE, + result.data[0].addr, + ) + result.flipVertical() + glReadBuffer(GL_BACK) +else: + proc takeScreenshot*(frame: Rect = rect(0, 0, 0, 0), + readFront: bool = true): Image = + Image() proc newOpenGLRenderer*(atlasSize: int, pixelScale = app.pixelScale): OpenGLRenderer = result = OpenGLRenderer() @@ -57,23 +68,27 @@ proc renderDrawable*(ctx: Context, node: Fig) = bx = node.screenBox.atXY(pos.x, pos.y) ctx.drawRect(bx, node.fill) -proc renderText(ctx: Context, node: Fig) {.forbids: [AppMainThreadEff].} = - ## draw characters (glyphs) +when not defined(js): + proc renderText(ctx: Context, node: Fig) {.forbids: [AppMainThreadEff].} = + ## draw characters (glyphs) - for glyph in node.textLayout.glyphs(): - if unicode.isWhiteSpace(glyph.rune): - # Don't draw space, even if font has a char for it. - # FIXME: use unicode 'is whitespace' ? - continue + for glyph in node.textLayout.glyphs(): + if unicode.isWhiteSpace(glyph.rune): + # Don't draw space, even if font has a char for it. + # FIXME: use unicode 'is whitespace' ? + continue - let - glyphId = glyph.hash() - charPos = vec2(glyph.pos.x, glyph.pos.y - glyph.descent * 1.0) - if glyphId notin ctx.entries: - trace "no glyph in context: ", - glyphId = glyphId, glyph = glyph.rune, glyphRepr = repr(glyph.rune) - continue - ctx.drawImage(glyphId, charPos, node.fill) + let + glyphId = glyph.hash() + charPos = vec2(glyph.pos.x, glyph.pos.y - glyph.descent * 1.0) + if glyphId notin ctx.entries: + trace "no glyph in context: ", + glyphId = glyphId, glyph = glyph.rune, glyphRepr = repr(glyph.rune) + continue + ctx.drawImage(glyphId, charPos, node.fill) +else: + proc renderText(ctx: Context, node: Fig) {.forbids: [AppMainThreadEff].} = + discard import macros except `$` @@ -117,7 +132,8 @@ macro postRender() = proc drawMasks(ctx: Context, node: Fig) = ctx.drawRoundedRectSdf( - rect = node.screenBox, color = rgba(255, 0, 0, 255).color, radii = node.corners + rect = node.screenBox, color = rgba(255, 0, 0, 255).color, + radii = node.corners ) proc renderDropShadows(ctx: Context, node: Fig) = @@ -274,14 +290,21 @@ proc renderBoxes(ctx: Context, node: Fig) = doStroke = true, ) -proc renderImage(ctx: Context, node: Fig) = - if node.image.id.int == 0: - return - let size = vec2(node.screenBox.w, node.screenBox.h) - #if ctx.cacheImage($node.image.name, node.image.id.Hash): - ctx.drawImage( - node.image.id.Hash, pos = node.screenBox.xy, color = node.image.color, size = size - ) +when not defined(js): + proc renderImage(ctx: Context, node: Fig) = + if node.image.id.int == 0: + return + let size = vec2(node.screenBox.w, node.screenBox.h) + #if ctx.cacheImage($node.image.name, node.image.id.Hash): + ctx.drawImage( + node.image.id.Hash, + pos = node.screenBox.xy, + color = node.image.color, + size = size, + ) +else: + proc renderImage(ctx: Context, node: Fig) = + discard proc render( ctx: Context, nodes: seq[Fig], nodeIdx, parentIdx: FigIdx @@ -365,18 +388,21 @@ proc render( # finally blocks will be run here, in reverse order postRender() -proc renderRoot*(ctx: Context, nodes: var Renders) {.forbids: [AppMainThreadEff].} = +proc renderRoot*(ctx: Context, nodes: var Renders) {.forbids: [ + AppMainThreadEff].} = ## draw roots for each level - var img: ImgObj - while imageChan.tryRecv(img): - debug "image loaded", id = $img.id.Hash - ctx.putImage(img) + when not defined(js): + var img: ImgObj + while imageChan.tryRecv(img): + debug "image loaded", id = $img.id.Hash + ctx.putImage(img) for zlvl, list in nodes.layers.pairs(): for rootIdx in list.rootIds: ctx.render(list.nodes, rootIdx, -1.FigIdx) -proc renderFrame*(renderer: OpenGLRenderer, nodes: var Renders, frameSize: Vec2) = +proc renderFrame*(renderer: OpenGLRenderer, nodes: var Renders, + frameSize: Vec2) = let ctx: Context = renderer.ctx clearColorBuffer(color(1.0, 1.0, 1.0, 1.0)) ctx.beginFrame(frameSize) @@ -389,7 +415,7 @@ proc renderFrame*(renderer: OpenGLRenderer, nodes: var Renders, frameSize: Vec2) ctx.restoreTransform() ctx.endFrame() - when defined(testOneFrame): + when defined(testOneFrame) and not defined(js): ## This is used for test only ## Take a screen shot of the first frame and exit. var img = takeScreenshot() @@ -409,7 +435,8 @@ proc renderOverlayFrame*( ctx.endFrame() proc renderFrame*( - ctx: Context, nodes: var Renders, frameSize: Vec2, pixelScale = ctx.pixelScale + ctx: Context, nodes: var Renders, frameSize: Vec2, + pixelScale = ctx.pixelScale ) = clearColorBuffer(color(1.0, 1.0, 1.0, 1.0)) ctx.beginFrame(frameSize) @@ -420,7 +447,8 @@ proc renderFrame*( ctx.endFrame() proc renderOverlayFrame*( - ctx: Context, nodes: var Renders, frameSize: Vec2, pixelScale = ctx.pixelScale + ctx: Context, nodes: var Renders, frameSize: Vec2, + pixelScale = ctx.pixelScale ) = ## Render without clearing the color buffer (useful for UI overlays). ctx.beginFrame(frameSize) diff --git a/src/figdraw/opengl/shaders.nim b/src/figdraw/opengl/shaders.nim index 1e6b9a0..77cd697 100644 --- a/src/figdraw/opengl/shaders.nim +++ b/src/figdraw/opengl/shaders.nim @@ -1,5 +1,5 @@ -import buffers, opengl, os, strformat, strutils, vmath, macros -import chronicles +import buffers, glapi, os, strformat, strutils, vmath, macros +import ../utils/logging type ShaderCompilationError* = object of CatchableError @@ -12,181 +12,252 @@ type name: string componentType: GLenum kind: BufferKind - values: array[64, uint8] - location: GLint + when defined(js): + valuesI: array[16, int32] + valuesF: array[16, float32] + else: + values: array[64, uint8] + location: GlUniformLocation changed: bool # Flag for if this uniform has changed since last bound. Shader* = ref object paths: seq[string] - programId*: GLuint + programId*: GlProgramId attribs*: seq[ShaderAttrib] uniforms*: seq[Uniform] -proc getErrorLog*( - id: GLuint, - path: string, - lenProc: typeof(glGetShaderiv), - strProc: typeof(glGetShaderInfoLog), -): string = - ## Gets the error log from compiling or linking shaders. - var length: GLint = 0 - lenProc(id, GL_INFO_LOG_LENGTH, length.addr) - var log = newString(length.int) - strProc(id, length, nil, cstring(log)) - when defined(emscripten): - result = log - else: - if log.startsWith("Compute info"): - log = log[25 ..^ 1] - let clickable = &"{path}({log[2..log.find(')')]}" - result = &"{clickable}: {log}" - -proc compileComputeShader*(compute: (string, string)): GLuint = - ## Compiles the compute shader and returns the program id. - var computeShader: GLuint - - block: - var computeShaderArray = allocCStringArray([compute[1]]) - defer: - dealloc(computeShaderArray) - - var isCompiled: GLint - - computeShader = glCreateShader(GL_COMPUTE_SHADER) - glShaderSource(computeShader, 1, computeShaderArray, nil) - glCompileShader(computeShader) - glGetShaderiv(computeShader, GL_COMPILE_STATUS, isCompiled.addr) - - if isCompiled == 0: - error "Compute shader compilation failed:", - logs = getErrorLog(computeShader, compute[0], glGetShaderiv, glGetShaderInfoLog) - quit(22) - - result = glCreateProgram() - glAttachShader(result, computeShader) - - glLinkProgram(result) - - var isLinked: GLint - glGetProgramiv(result, GL_LINK_STATUS, isLinked.addr) - if isLinked == 0: - let logs = getErrorLog(result, compute[0], glGetProgramiv, glGetProgramInfoLog) - error "Linking compute shader failed:", logs = logs - quit(33) - -proc compileComputeShader*(path: string): GLuint = - ## Compiles the compute shader and returns the program id. - compileComputeShader((path, readFile(path))) - -proc compileShaderFiles*(vert, frag: (string, string)): GLuint = - ## Compiles the shader files and links them into a program, returning that id. - var vertShader, fragShader: GLuint - - # Compile the shaders - block shaders: - var vertShaderArray = allocCStringArray([vert[1]]) - var fragShaderArray = allocCStringArray([frag[1]]) - - defer: - dealloc(vertShaderArray) - dealloc(fragShaderArray) - - var isCompiled: GLint +when not defined(js): + proc getErrorLog*( + id: GlProgramId, + path: string, + lenProc: typeof(glGetShaderiv), + strProc: typeof(glGetShaderInfoLog), + ): string = + ## Gets the error log from compiling or linking shaders. + var length: GLint = 0 + lenProc(id, GL_INFO_LOG_LENGTH, length.addr) + var log = newString(length.int) + strProc(id, length, nil, cstring(log)) + when defined(emscripten): + result = log + else: + if log.startsWith("Compute info"): + log = log[25 ..^ 1] + let clickable = &"{path}({log[2..log.find(')')]}" + result = &"{clickable}: {log}" + +when not defined(js): + proc compileComputeShader*(compute: (string, string)): GlProgramId = + ## Compiles the compute shader and returns the program id. + var computeShader: GlShaderId + + block: + var computeShaderArray = allocCStringArray([compute[1]]) + defer: + dealloc(computeShaderArray) + + var isCompiled: GLint + + computeShader = glCreateShader(GL_COMPUTE_SHADER) + glShaderSource(computeShader, 1, computeShaderArray, nil) + glCompileShader(computeShader) + glGetShaderiv(computeShader, GL_COMPILE_STATUS, isCompiled.addr) + + if isCompiled == 0: + error "Compute shader compilation failed:", + logs = getErrorLog(computeShader, compute[0], glGetShaderiv, + glGetShaderInfoLog) + quit(22) + + result = glCreateProgram() + glAttachShader(result, computeShader) + + glLinkProgram(result) + + var isLinked: GLint + glGetProgramiv(result, GL_LINK_STATUS, isLinked.addr) + if isLinked == 0: + let logs = getErrorLog(result, compute[0], glGetProgramiv, glGetProgramInfoLog) + error "Linking compute shader failed:", logs = logs + quit(33) - vertShader = glCreateShader(GL_VERTEX_SHADER) - glShaderSource(vertShader, 1, vertShaderArray, nil) + proc compileComputeShader*(path: string): GlProgramId = + ## Compiles the compute shader and returns the program id. + compileComputeShader((path, readFile(path))) +else: + proc compileComputeShader*(compute: (string, string)): GlProgramId = + raise newException(ShaderCompilationError, "Compute shaders not supported on JS") + + proc compileComputeShader*(path: string): GlProgramId = + raise newException(ShaderCompilationError, "Compute shaders not supported on JS") + +when defined(js): + proc compileShaderFiles*(vert, frag: (string, string)): GlProgramId = + ## Compiles the shader files and links them into a program, returning that id. + let vertShader = glCreateShader(GL_VERTEX_SHADER) + glShaderSource(vertShader, cstring(vert[1])) glCompileShader(vertShader) - glGetShaderiv(vertShader, GL_COMPILE_STATUS, isCompiled.addr) - - if isCompiled == 0: - let logs = getErrorLog(vertShader, vert[0], glGetShaderiv, glGetShaderInfoLog) - if "GLSL 3.30 is not supported" in logs: - warn "Vertex shader compilation failed", - reason = "GLSL 3.30 is not supported", - advice = "Try compiling with `-d:useOpenGlEs`" - raise newException(ShaderCompilationError, "Vertex shader compilation failed") - else: - error "Vertex shader compilation failed:", logs = logs - quit(33) + if not glGetShaderParameter(vertShader, GL_COMPILE_STATUS): + let logs = glGetShaderInfoLog(vertShader) + error "Vertex shader compilation failed:", logs = logs + quit(33) - fragShader = glCreateShader(GL_FRAGMENT_SHADER) - glShaderSource(fragShader, 1, fragShaderArray, nil) + let fragShader = glCreateShader(GL_FRAGMENT_SHADER) + glShaderSource(fragShader, cstring(frag[1])) glCompileShader(fragShader) - glGetShaderiv(fragShader, GL_COMPILE_STATUS, isCompiled.addr) - - if isCompiled == 0: - let logs = getErrorLog(fragShader, frag[0], glGetShaderiv, glGetShaderInfoLog) + if not glGetShaderParameter(fragShader, GL_COMPILE_STATUS): + let logs = glGetShaderInfoLog(fragShader) error "Fragment shader compilation failed:", logs = logs quit(33) - # Attach shaders to a GL program - result = glCreateProgram() - glAttachShader(result, vertShader) - glAttachShader(result, fragShader) + result = glCreateProgram() + glAttachShader(result, vertShader) + glAttachShader(result, fragShader) + glLinkProgram(result) + + if glGetProgramParameter(result, GL_LINK_STATUS) == 0: + let logs = glGetProgramInfoLog(result) + error "Linking shaders failed:", logs = logs + quit(44) +else: + proc compileShaderFiles*(vert, frag: (string, string)): GlProgramId = + ## Compiles the shader files and links them into a program, returning that id. + var vertShader, fragShader: GlShaderId + + # Compile the shaders + block shaders: + var vertShaderArray = allocCStringArray([vert[1]]) + var fragShaderArray = allocCStringArray([frag[1]]) + + defer: + dealloc(vertShaderArray) + dealloc(fragShaderArray) + + var isCompiled: GLint + + vertShader = glCreateShader(GL_VERTEX_SHADER) + glShaderSource(vertShader, 1, vertShaderArray, nil) + glCompileShader(vertShader) + glGetShaderiv(vertShader, GL_COMPILE_STATUS, isCompiled.addr) + + if isCompiled == 0: + let logs = getErrorLog(vertShader, vert[0], glGetShaderiv, glGetShaderInfoLog) + if "GLSL 3.30 is not supported" in logs: + warn "Vertex shader compilation failed", + reason = "GLSL 3.30 is not supported", + advice = "Try compiling with `-d:useOpenGlEs`" + raise newException(ShaderCompilationError, "Vertex shader compilation failed") + else: + error "Vertex shader compilation failed:", logs = logs + quit(33) + + fragShader = glCreateShader(GL_FRAGMENT_SHADER) + glShaderSource(fragShader, 1, fragShaderArray, nil) + glCompileShader(fragShader) + glGetShaderiv(fragShader, GL_COMPILE_STATUS, isCompiled.addr) + + if isCompiled == 0: + let logs = getErrorLog(fragShader, frag[0], glGetShaderiv, glGetShaderInfoLog) + error "Fragment shader compilation failed:", logs = logs + quit(33) + + # Attach shaders to a GL program + result = glCreateProgram() + glAttachShader(result, vertShader) + glAttachShader(result, fragShader) - glLinkProgram(result) + glLinkProgram(result) - var isLinked: GLint - glGetProgramiv(result, GL_LINK_STATUS, isLinked.addr) - if isLinked == 0: - let logs = getErrorLog(result, "", glGetProgramiv, glGetProgramInfoLog) - error "Linking shaders failed:", logs = logs - quit(44) + var isLinked: GLint + glGetProgramiv(result, GL_LINK_STATUS, isLinked.addr) + if isLinked == 0: + let logs = getErrorLog(result, "", glGetProgramiv, glGetProgramInfoLog) + error "Linking shaders failed:", logs = logs + quit(44) -proc compileShaderFiles*(vertPath, fragPath: string): GLuint = +proc compileShaderFiles*(vertPath, fragPath: string): GlProgramId = ## Compiles the shader files and links them into a program, returning that id. compileShaderFiles((vertPath, readFile(vertPath)), (fragPath, readFile(fragPath))) proc readAttribsAndUniforms(shader: Shader) = - block attributes: - var activeAttribCount: GLint - glGetProgramiv(shader.programId, GL_ACTIVE_ATTRIBUTES, activeAttribCount.addr) - - for i in 0 ..< activeAttribCount: - var - buf = newString(64) - length, size: GLint - kind: GLenum - glGetActiveAttrib( - shader.programId, - i.GLuint, - len(buf).GLint, - length.addr, - size.addr, - kind.addr, - cstring(buf), - ) - buf.setLen(length) - - let location = glGetAttribLocation(shader.programId, cstring(buf)) - shader.attribs.add(ShaderAttrib(name: move(buf), location: location)) - - block uniforms: - var activeUniformCount: GLint - glGetProgramiv(shader.programId, GL_ACTIVE_UNIFORMS, activeUniformCount.addr) - - for i in 0 ..< activeUniformCount: - var - buf = newString(64) - length, size: GLint - kind: GLenum - glGetActiveUniform( - shader.programId, - i.GLuint, - len(buf).GLint, - length.addr, - size.addr, - kind.addr, - cstring(buf), - ) - buf.setLen(length) - - if buf.endsWith("[0]"): - # Skip arrays, these are part of UBOs and done a different way - continue - - let location = glGetUniformLocation(shader.programId, cstring(buf)) - shader.uniforms.add(Uniform(name: move(buf), location: location)) + when defined(js): + block attributes: + let activeAttribCount = + glGetProgramParameter(shader.programId, GL_ACTIVE_ATTRIBUTES) + + for i in 0 ..< activeAttribCount: + let info = glGetActiveAttrib(shader.programId, i.GLuint) + if info.isNil: + continue + let name = $info.name + let location = glGetAttribLocation(shader.programId, info.name) + shader.attribs.add(ShaderAttrib(name: name, location: location)) + + block uniforms: + let activeUniformCount = + glGetProgramParameter(shader.programId, GL_ACTIVE_UNIFORMS) + + for i in 0 ..< activeUniformCount: + let info = glGetActiveUniform(shader.programId, i.GLuint) + if info.isNil: + continue + let name = $info.name + if name.endsWith("[0]"): + continue + let location = glGetUniformLocation(shader.programId, info.name) + shader.uniforms.add(Uniform(name: name, location: location)) + else: + block attributes: + var activeAttribCount: GLint + glGetProgramiv(shader.programId, GL_ACTIVE_ATTRIBUTES, + activeAttribCount.addr) + + for i in 0 ..< activeAttribCount: + var + buf = newString(64) + length, size: GLint + kind: GLenum + glGetActiveAttrib( + shader.programId, + i.GLuint, + len(buf).GLint, + length.addr, + size.addr, + kind.addr, + cstring(buf), + ) + buf.setLen(length) + + let location = glGetAttribLocation(shader.programId, cstring(buf)) + shader.attribs.add(ShaderAttrib(name: move(buf), location: location)) + + block uniforms: + var activeUniformCount: GLint + glGetProgramiv(shader.programId, GL_ACTIVE_UNIFORMS, + activeUniformCount.addr) + + for i in 0 ..< activeUniformCount: + var + buf = newString(64) + length, size: GLint + kind: GLenum + glGetActiveUniform( + shader.programId, + i.GLuint, + len(buf).GLint, + length.addr, + size.addr, + kind.addr, + cstring(buf), + ) + buf.setLen(length) + + if buf.endsWith("[0]"): + # Skip arrays, these are part of UBOs and done a different way + continue + + let location = glGetUniformLocation(shader.programId, cstring(buf)) + shader.uniforms.add(Uniform(name: move(buf), location: location)) proc newShader*(compute: (string, string)): Shader = result = Shader() @@ -242,24 +313,25 @@ proc hasUniform*(shader: Shader, name: string): bool = return true return false -proc setUniform( - shader: Shader, - name: string, - componentType: GLenum, - kind: BufferKind, - values: array[64, uint8], -) = - for uniform in shader.uniforms.mitems: - if uniform.name == name: - if uniform.componentType != componentType or uniform.kind != kind or - uniform.values != values: - uniform.componentType = componentType - uniform.kind = kind - uniform.values = values - uniform.changed = true - return - - warn "Ignoring setUniform, not active", name = $name +when not defined(js): + proc setUniform( + shader: Shader, + name: string, + componentType: GLenum, + kind: BufferKind, + values: array[64, uint8], + ) = + for uniform in shader.uniforms.mitems: + if uniform.name == name: + if uniform.componentType != componentType or uniform.kind != kind or + uniform.values != values: + uniform.componentType = componentType + uniform.kind = kind + uniform.values = values + uniform.changed = true + return + + warn "Ignoring setUniform, not active", name = $name proc setUniform( shader: Shader, @@ -269,7 +341,19 @@ proc setUniform( values: array[16, float32], ) = assert componentType == cGL_FLOAT - setUniform(shader, name, componentType, kind, cast[array[64, uint8]](values)) + when defined(js): + for uniform in shader.uniforms.mitems: + if uniform.name == name: + if uniform.componentType != componentType or uniform.kind != kind or + uniform.valuesF != values: + uniform.componentType = componentType + uniform.kind = kind + uniform.valuesF = values + uniform.changed = true + return + warn "Ignoring setUniform, not active", name = $name + else: + setUniform(shader, name, componentType, kind, cast[array[64, uint8]](values)) proc setUniform( shader: Shader, @@ -279,7 +363,19 @@ proc setUniform( values: array[16, int32], ) = assert componentType == cGL_INT - setUniform(shader, name, componentType, kind, cast[array[64, uint8]](values)) + when defined(js): + for uniform in shader.uniforms.mitems: + if uniform.name == name: + if uniform.componentType != componentType or uniform.kind != kind or + uniform.valuesI != values: + uniform.componentType = componentType + uniform.kind = kind + uniform.valuesI = values + uniform.changed = true + return + warn "Ignoring setUniform, not active", name = $name + else: + setUniform(shader, name, componentType, kind, cast[array[64, uint8]](values)) proc raiseUniformVarargsException(name: string, count: int) = raise newException( @@ -350,7 +446,20 @@ proc setUniform*(shader: Shader, name: string, v: Vec4) = shader.setUniform(name, cGL_FLOAT, bkVEC4, values) proc setUniform*(shader: Shader, name: string, m: Mat4) = - shader.setUniform(name, cGL_FLOAT, bkMAT4, cast[array[16, float32]](m)) + when defined(js): + var values: array[16, float32] + when compiles(m.arr): + for i in 0 .. 15: + values[i] = m.arr[i] + else: + var idx = 0 + for r in 0 .. 3: + for c in 0 .. 3: + values[idx] = m[r, c] + inc idx + shader.setUniform(name, cGL_FLOAT, bkMAT4, values) + else: + shader.setUniform(name, cGL_FLOAT, bkMAT4, cast[array[16, float32]](m)) proc setUniform*(shader: Shader, name: string, b: bool) = var values: array[16, int32] @@ -366,7 +475,10 @@ proc bindUniforms*(shader: Shader) = continue if uniform.componentType == cGL_INT: - let values = cast[array[16, GLint]](uniform.values) + when defined(js): + let values = uniform.valuesI + else: + let values = cast[array[16, GLint]](uniform.values) case uniform.kind of bkSCALAR: glUniform1i(uniform.location, values[0]) @@ -379,7 +491,10 @@ proc bindUniforms*(shader: Shader) = else: raiseUniformKindException(uniform.name, uniform.kind) elif uniform.componentType == cGL_FLOAT: - let values = cast[array[16, float32]](uniform.values) + when defined(js): + let values = uniform.valuesF + else: + let values = cast[array[16, float32]](uniform.values) case uniform.kind of bkSCALAR: glUniform1f(uniform.location, values[0]) @@ -390,7 +505,10 @@ proc bindUniforms*(shader: Shader) = of bkVEC4: glUniform4f(uniform.location, values[0], values[1], values[2], values[3]) of bkMAT4: - glUniformMatrix4fv(uniform.location, 1, GL_FALSE, values[0].unsafeAddr) + when defined(js): + glUniformMatrix4fv(uniform.location, 1, GL_FALSE, values) + else: + glUniformMatrix4fv(uniform.location, 1, GL_FALSE, values[0].unsafeAddr) else: raiseUniformKindException(uniform.name, uniform.kind) else: @@ -398,7 +516,8 @@ proc bindUniforms*(shader: Shader) = uniform.changed = false -proc bindUniformBuffer*(shader: Shader, name: string, buffer: Buffer, binding: GLuint) = +proc bindUniformBuffer*(shader: Shader, name: string, buffer: Buffer, + binding: GLuint) = assert buffer.target == GL_UNIFORM_BUFFER let index = glGetUniformBlockIndex(shader.programId, name) glBindBufferBase(GL_UNIFORM_BUFFER, binding, buffer.bufferId) @@ -411,22 +530,41 @@ proc bindAttrib*(shader: Shader, name: string, buffer: Buffer) = if name == attrib.name: if buffer.componentType == cGL_FLOAT or buffer.normalized or buffer.kind != bkSCALAR: - glVertexAttribPointer( - attrib.location.GLuint, - buffer.kind.componentCount().GLint, - buffer.componentType, - if buffer.normalized: GL_TRUE else: GL_FALSE, - 0, - nil, - ) + when defined(js): + glVertexAttribPointer( + attrib.location.GLuint, + int32(buffer.kind.componentCount()), + buffer.componentType, + if buffer.normalized: GL_TRUE else: GL_FALSE, + 0, + 0.GLintptr, + ) + else: + glVertexAttribPointer( + attrib.location.GLuint, + buffer.kind.componentCount().GLint, + buffer.componentType, + if buffer.normalized: GL_TRUE else: GL_FALSE, + 0, + nil, + ) else: - glVertexAttribIPointer( - attrib.location.GLuint, - buffer.kind.componentCount().GLint, - buffer.componentType, - 0, - nil, - ) + when defined(js): + glVertexAttribIPointer( + attrib.location.GLuint, + int32(buffer.kind.componentCount()), + buffer.componentType, + 0, + 0.GLintptr, + ) + else: + glVertexAttribIPointer( + attrib.location.GLuint, + buffer.kind.componentCount().GLint, + buffer.componentType, + 0, + nil, + ) glEnableVertexAttribArray(attrib.location.GLuint) return diff --git a/src/figdraw/opengl/textures.nim b/src/figdraw/opengl/textures.nim index 2e49360..b9e7da2 100644 --- a/src/figdraw/opengl/textures.nim +++ b/src/figdraw/opengl/textures.nim @@ -1,4 +1,10 @@ -import buffers, pixie, opengl +import buffers, glapi +when not defined(js): + import pixie +else: + type Image* = object +when defined(js): + import std/jsffi type MinFilter* = enum @@ -28,42 +34,80 @@ type magFilter*: MagFilter wrapS*, wrapT*: Wrap genMipmap*: bool - textureId*: GLuint + textureId*: GlTextureId -proc bindTextureBufferData*(texture: ptr Texture, buffer: ptr Buffer, data: pointer) = - bindBufferData(buffer, data) +when not defined(js): + proc bindTextureBufferData*(texture: ptr Texture, buffer: ptr Buffer, + data: pointer) = + bindBufferData(buffer, data) - if texture.textureId == 0: - glGenTextures(1, texture.textureId.addr) + if texture.textureId == 0: + glGenTextures(1, texture.textureId.addr) - glBindTexture(GL_TEXTURE_BUFFER, texture.textureId) - glTexBuffer(GL_TEXTURE_BUFFER, texture.internalFormat, buffer.bufferId) + glBindTexture(GL_TEXTURE_BUFFER, texture.textureId) + glTexBuffer(GL_TEXTURE_BUFFER, texture.internalFormat, buffer.bufferId) +else: + proc bindTextureBufferData*(texture: ptr Texture, buffer: ptr Buffer, + data: pointer) = + discard proc bindTextureData*(texture: ptr Texture, data: pointer) = - if texture.textureId == 0: - glGenTextures(1, texture.textureId.addr) + when defined(js): + if texture.textureId.isNil: + texture.textureId = glCreateTexture() + else: + if texture.textureId == 0: + glGenTextures(1, texture.textureId.addr) glBindTexture(GL_TEXTURE_2D, texture.textureId) - glTexImage2D( - target = GL_TEXTURE_2D, - level = 0, - internalFormat = texture.internalFormat.GLint, - width = texture.width, - height = texture.height, - border = 0, - format = texture.format, - `type` = texture.componentType, - pixels = data, - ) - - if texture.magFilter != magDefault: - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, texture.magFilter.GLint) - if texture.minFilter != minDefault: - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, texture.minFilter.GLint) - if texture.wrapS != wDefault: - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texture.wrapS.GLint) - if texture.wrapT != wDefault: - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texture.wrapT.GLint) + when defined(js): + let jsData = if data.isNil: jsNull else: cast[JsObject](data) + glTexImage2D( + GL_TEXTURE_2D, + 0, + int32(texture.internalFormat), + texture.width, + texture.height, + 0, + texture.format, + texture.componentType, + jsData, + ) + else: + glTexImage2D( + target = GL_TEXTURE_2D, + level = 0, + internalFormat = texture.internalFormat.GLint, + width = texture.width, + height = texture.height, + border = 0, + format = texture.format, + `type` = texture.componentType, + pixels = data, + ) + + when defined(js): + if texture.magFilter != magDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, + int32(texture.magFilter)) + if texture.minFilter != minDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + int32(texture.minFilter)) + if texture.wrapS != wDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, int32(texture.wrapS)) + if texture.wrapT != wDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, int32(texture.wrapT)) + else: + if texture.magFilter != magDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, + texture.magFilter.GLint) + if texture.minFilter != minDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + texture.minFilter.GLint) + if texture.wrapS != wDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texture.wrapS.GLint) + if texture.wrapT != wDefault: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texture.wrapT.GLint) if texture.genMipmap: glGenerateMipmap(GL_TEXTURE_2D) @@ -71,49 +115,57 @@ proc bindTextureData*(texture: ptr Texture, data: pointer) = func getFormat(image: Image): GLenum = result = GL_RGBA -proc initTexture*(image: Image): Texture = - result.width = image.width.GLint - result.height = image.height.GLint - result.componentType = GL_UNSIGNED_BYTE - result.format = image.getFormat() - result.internalFormat = GL_RGBA8 - result.genMipmap = true - result.minFilter = minLinearMipmapLinear - result.magFilter = magLinear - var data = newSeq[ColorRGBA](image.width * image.height) - for i in 0 ..< data.len: - data[i] = image.data[i] - bindTextureData(result.addr, data[0].addr) - -proc updateSubImage*(texture: Texture, x, y: int, image: Image, level: int) = - ## Update a small part of a texture image. - var data = newSeq[ColorRGBA](image.width * image.height) - for i in 0 ..< data.len: - data[i] = image.data[i] - glBindTexture(GL_TEXTURE_2D, texture.textureId) - glTexSubImage2D( - GL_TEXTURE_2D, - level = level.GLint, - xoffset = x.GLint, - yoffset = y.GLint, - width = image.width.GLint, - height = image.height.GLint, - format = image.getFormat(), - `type` = GL_UNSIGNED_BYTE, - pixels = data[0].addr, - ) - -proc updateSubImage*(texture: Texture, x, y: int, image: Image) = - ## Update a small part of texture with a new image. - var - x = x - y = y - image = image - level = 0 - - while image.width > 1 and image.height > 1: - texture.updateSubImage(x, y, image, level) - image = image.minifyBy2() - x = x div 2 - y = y div 2 - inc(level) +when not defined(js): + proc initTexture*(image: Image): Texture = + result.width = image.width.GLint + result.height = image.height.GLint + result.componentType = GL_UNSIGNED_BYTE + result.format = image.getFormat() + result.internalFormat = GL_RGBA8 + result.genMipmap = true + result.minFilter = minLinearMipmapLinear + result.magFilter = magLinear + var data = newSeq[ColorRGBA](image.width * image.height) + for i in 0 ..< data.len: + data[i] = image.data[i] + bindTextureData(result.addr, data[0].addr) + +when not defined(js): + proc updateSubImage*(texture: Texture, x, y: int, image: Image, level: int) = + ## Update a small part of a texture image. + var data = newSeq[ColorRGBA](image.width * image.height) + for i in 0 ..< data.len: + data[i] = image.data[i] + glBindTexture(GL_TEXTURE_2D, texture.textureId) + glTexSubImage2D( + GL_TEXTURE_2D, + level = level.GLint, + xoffset = x.GLint, + yoffset = y.GLint, + width = image.width.GLint, + height = image.height.GLint, + format = image.getFormat(), + `type` = GL_UNSIGNED_BYTE, + pixels = data[0].addr, + ) + + proc updateSubImage*(texture: Texture, x, y: int, image: Image) = + ## Update a small part of texture with a new image. + var + x = x + y = y + image = image + level = 0 + + while image.width > 1 and image.height > 1: + texture.updateSubImage(x, y, image, level) + image = image.minifyBy2() + x = x div 2 + y = y div 2 + inc(level) +else: + proc updateSubImage*(texture: Texture, x, y: int, image: Image, level: int) = + discard + + proc updateSubImage*(texture: Texture, x, y: int, image: Image) = + discard diff --git a/src/figdraw/utils/chronicles_stub.nim b/src/figdraw/utils/chronicles_stub.nim new file mode 100644 index 0000000..f650f16 --- /dev/null +++ b/src/figdraw/utils/chronicles_stub.nim @@ -0,0 +1,17 @@ +template logScope*(body: untyped) = + discard + +template trace*(args: varargs[untyped]) = + discard + +template debug*(args: varargs[untyped]) = + discard + +template info*(args: varargs[untyped]) = + discard + +template warn*(args: varargs[untyped]) = + discard + +template error*(args: varargs[untyped]) = + discard diff --git a/src/figdraw/utils/drawextras.nim b/src/figdraw/utils/drawextras.nim index b6073bb..919816a 100644 --- a/src/figdraw/utils/drawextras.nim +++ b/src/figdraw/utils/drawextras.nim @@ -5,7 +5,7 @@ import ../commons import ../fignodes import pkg/chroma -import pkg/chronicles +import logging import pkg/pixie import ./drawutils diff --git a/src/figdraw/utils/drawshadows.nim b/src/figdraw/utils/drawshadows.nim index b4cf62c..afaf420 100644 --- a/src/figdraw/utils/drawshadows.nim +++ b/src/figdraw/utils/drawshadows.nim @@ -3,7 +3,7 @@ import std/hashes import std/fenv import pkg/chroma -import pkg/chronicles +import logging import pkg/pixie import pkg/sdfy @@ -140,11 +140,11 @@ proc fillRoundedRectWithShadowSdf*[R]( dcTopRight: xy + vec2(w - cornerCbs[dcTopRight].inner.float32, 0), dcBottomLeft: xy + vec2(0, h - cornerCbs[dcBottomLeft].inner.float32), dcBottomRight: - xy + - vec2( - w - cornerCbs[dcBottomRight].inner.float32, - h - cornerCbs[dcBottomRight].inner.float32, - ), + xy + + vec2( + w - cornerCbs[dcBottomRight].inner.float32, + h - cornerCbs[dcBottomRight].inner.float32, + ), ] coffset = [ @@ -156,10 +156,12 @@ proc fillRoundedRectWithShadowSdf*[R]( ccenter = [ dcTopLeft: vec2( - cornerCbs[dcTopLeft].sideSize.float32, cornerCbs[dcTopLeft].sideSize.float32 + cornerCbs[dcTopLeft].sideSize.float32, cornerCbs[ + dcTopLeft].sideSize.float32 ), dcTopRight: vec2( - cornerCbs[dcTopRight].sideSize.float32, cornerCbs[dcTopRight].sideSize.float32 + cornerCbs[dcTopRight].sideSize.float32, cornerCbs[ + dcTopRight].sideSize.float32 ), dcBottomLeft: vec2( cornerCbs[dcBottomLeft].sideSize.float32, @@ -191,7 +193,8 @@ proc fillRoundedRectWithShadowSdf*[R]( ] let sides = - [dcTopLeft: dLeft, dcTopRight: dTop, dcBottomLeft: dBottom, dcBottomRight: dRight] + [dcTopLeft: dLeft, dcTopRight: dTop, dcBottomLeft: dBottom, + dcBottomRight: dRight] let prevCorner = [ dcTopLeft: dcBottomLeft, dcTopRight: dcTopLeft, @@ -238,7 +241,8 @@ proc fillRoundedRectWithShadowSdf*[R]( rect(paddingOffset, paddingOffset + inner, inner, sideDelta), shadowColor ) ctx.drawRect( - rect(paddingOffset + inner, paddingOffset, sideDelta, cbs.maxRadius.float32), + rect(paddingOffset + inner, paddingOffset, sideDelta, + cbs.maxRadius.float32), shadowColor, ) @@ -251,7 +255,8 @@ proc fillRoundedRectWithShadowSdf*[R]( ctx.drawImageAdj( sideHashes[sides[corner]], vec2( - paddingOffset, paddingOffset + cornerCbs[prevCorner[corner]].inner.float32 + paddingOffset, paddingOffset + cornerCbs[prevCorner[ + corner]].inner.float32 ), shadowColor, borderSize, @@ -270,7 +275,8 @@ proc fillRoundedRectWithShadowSdf*[R]( rect(maxRadius.float32, 0, w - 2 * maxRadius.float32, h), shadowColor ) ctx.drawRect( - rect(0, 0 + maxRadius.float32, maxRadius.float32, h - 2 * maxRadius.float32), + rect(0, 0 + maxRadius.float32, maxRadius.float32, h - 2 * + maxRadius.float32), shadowColor, ) ctx.drawRect( diff --git a/src/figdraw/utils/drawutils.nim b/src/figdraw/utils/drawutils.nim index 467f519..8d0fd73 100644 --- a/src/figdraw/utils/drawutils.nim +++ b/src/figdraw/utils/drawutils.nim @@ -4,7 +4,7 @@ import ../commons import ../fignodes import pkg/chroma -import pkg/chronicles +import logging proc hash(v: Vec2): Hash = hash((v.x, v.y)) @@ -32,7 +32,8 @@ proc getCircleBoxSizes*( width = float32.high(), height = float32.high(), innerShadow = false, -): tuple[maxRadius, sideSize, totalSize, padding, paddingOffset, inner, weightSize: int] = +): tuple[maxRadius, sideSize, totalSize, padding, paddingOffset, inner, + weightSize: int] = result.maxRadius = 0 for r in radii: result.maxRadius = max(result.maxRadius, r.round().int) @@ -57,11 +58,13 @@ proc getCircleBoxSizes*( proc roundedBoxCornerSizes*( cbs: tuple[ - maxRadius, sideSize, totalSize, padding, paddingOffset, inner, weightSize: int + maxRadius, sideSize, totalSize, padding, paddingOffset, inner, + weightSize: int ], radii: array[DirectionCorners, float32], innerShadow: bool, -): array[DirectionCorners, tuple[radius, sideSize, inner, sideDelta, center: int]] = +): array[DirectionCorners, tuple[radius, sideSize, inner, sideDelta, + center: int]] = let ww = cbs.weightSize for corner in DirectionCorners: diff --git a/src/figdraw/utils/glutils.nim b/src/figdraw/utils/glutils.nim index 8e851c6..e58be08 100644 --- a/src/figdraw/utils/glutils.nim +++ b/src/figdraw/utils/glutils.nim @@ -1,4 +1,4 @@ -import pkg/opengl +import ../opengl/glapi import pkg/chroma const @@ -66,7 +66,7 @@ proc useDepthBuffer*(on: bool) = glDisable(GL_DEPTH_TEST) proc startOpenGL*(openglVersion: (int, int)) = - when not defined(emscripten): + when not defined(emscripten) and not defined(js): loadExtensions() openglDebug() diff --git a/src/figdraw/utils/logging.nim b/src/figdraw/utils/logging.nim new file mode 100644 index 0000000..46cbc07 --- /dev/null +++ b/src/figdraw/utils/logging.nim @@ -0,0 +1,6 @@ +when defined(js): + import chronicles_stub + export chronicles_stub +else: + import pkg/chronicles + export chronicles diff --git a/src/figdraw/webgl/api.nim b/src/figdraw/webgl/api.nim index 5d67898..43188e6 100644 --- a/src/figdraw/webgl/api.nim +++ b/src/figdraw/webgl/api.nim @@ -4,9 +4,10 @@ when not defined(js) and not defined(nimsuggest): import std/[dom, jsffi] type - GLenum* = uint32 + # Use signed 32-bit integers in JS to avoid BigInt interop in WebGL calls. + GLenum* = int32 GLboolean* = bool - GLbitfield* = uint32 + GLbitfield* = int32 GLbyte* = int8 GLshort* = int16 GLint* = int32 @@ -16,7 +17,7 @@ type GLsizeiptr* = int32 GLubyte* = uint8 GLushort* = uint16 - GLuint* = uint32 + GLuint* = int32 GLfloat* = float32 GLclampf* = float32 GLint64* = int64 @@ -26,6 +27,9 @@ type WebGLRenderingContext* {.importc.} = ref object of JsRoot WebGL2RenderingContext* {.importc.} = ref object of WebGLRenderingContext WebGLActiveInfo* {.importc.} = ref object of JsRoot + size*: int + `type`* {.importc: "type".}: GLenum + name*: cstring WebGLBuffer* {.importc.} = ref object of JsRoot WebGLContextEvent* {.importc.} = ref object of Event WebGLFramebuffer* {.importc.} = ref object of JsRoot @@ -48,6 +52,7 @@ type Float32Array* {.importc.} = ref object of JsRoot Uint16Array* {.importc.} = ref object of JsRoot Uint32Array* {.importc.} = ref object of JsRoot + Uint8Array* {.importc.} = ref object of JsRoot type WebGLExtension* = ref object of JsRoot @@ -155,6 +160,9 @@ proc newUint16Array*(data: openArray[uint16]): Uint16Array proc newUint32Array*(data: openArray[uint32]): Uint32Array {.importjs: "new Uint32Array(#)".} +proc newUint8Array*(data: openArray[uint8]): Uint8Array + {.importjs: "new Uint8Array(#)".} + proc getContext*(canvas: HTMLCanvasElement; contextId: cstring): WebGLRenderingContext {.importjs: "#.getContext(#)".} From 3d5197eaf6e1d0810c6e184e499107439db83755 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:19:05 -0700 Subject: [PATCH 04/10] add webgl --- examples/webgl_renderlist_100.html | 22 + examples/webgl_renderlist_100.nim | 77 +++ src/figdraw/common/fontutils.nim | 995 +++++++++++++++------------- src/figdraw/common/fontutils_js.nim | 28 - src/figdraw/commons.nim | 3 +- 5 files changed, 620 insertions(+), 505 deletions(-) create mode 100644 examples/webgl_renderlist_100.html create mode 100644 examples/webgl_renderlist_100.nim delete mode 100644 src/figdraw/common/fontutils_js.nim diff --git a/examples/webgl_renderlist_100.html b/examples/webgl_renderlist_100.html new file mode 100644 index 0000000..9a53cb5 --- /dev/null +++ b/examples/webgl_renderlist_100.html @@ -0,0 +1,22 @@ + + + + + + FigDraw WebGL RenderList 100 (Nim JS) + + + + + + + diff --git a/examples/webgl_renderlist_100.nim b/examples/webgl_renderlist_100.nim new file mode 100644 index 0000000..a89ef9f --- /dev/null +++ b/examples/webgl_renderlist_100.nim @@ -0,0 +1,77 @@ +when not defined(js) and not defined(nimsuggest): + {.fatal: "This example requires the Nim JS backend (nim js).".} + +import std/[dom, jsconsole, jsffi] +import chroma + +import figdraw/commons +import figdraw/fignodes +import figdraw/opengl/renderer as glrenderer +import figdraw/opengl/glapi +import figdraw/utils/glutils +import figdraw/webgl/api as webgl + +import renderlist_100_common + +var globalFrame = 0 + +proc main() = + app.running = true + app.autoUiScale = false + app.uiScale = 1.0 + app.pixelScale = 1.0 + + let canvas = webgl.asCanvas(document.createElement("canvas")) + document.body.appendChild(canvas) + + document.body.style.margin = "0" + document.body.style.overflow = "hidden" + document.body.style.background = "#0c0f16" + canvas.style.display = "block" + + let gl = cast[glapi.WebGL2RenderingContext](canvas.getContext("webgl2")) + if gl.isNull or gl.isUndefined: + console.error("WebGL2 not available") + return + + setWebGLContext(gl) + startOpenGL(openglVersion) + + let renderer = glrenderer.newOpenGLRenderer( + atlasSize = when not defined(useFigDrawTextures): 1024 else: 2048, + pixelScale = app.pixelScale, + ) + + proc updateCanvas(): Vec2 = + let dpr = if window.devicePixelRatio <= 0: 1.0 else: window.devicePixelRatio + let cssWidth = max(window.innerWidth, 1) + let cssHeight = max(window.innerHeight, 1) + + app.pixelScale = dpr.float32 + renderer.ctx.pixelScale = app.pixelScale + + let pixelWidth = int(cssWidth.float * dpr) + let pixelHeight = int(cssHeight.float * dpr) + if canvas.width != pixelWidth: + canvas.width = pixelWidth + if canvas.height != pixelHeight: + canvas.height = pixelHeight + canvas.style.width = cstring($cssWidth & "px") + canvas.style.height = cstring($cssHeight & "px") + + result = vec2(cssWidth.float32, cssHeight.float32) + + proc drawFrame(time: float) = + inc globalFrame + let cssSize = updateCanvas() + var renders = makeRenderTree(cssSize.x, cssSize.y, globalFrame) + renderer.renderFrame( + renders, + vec2(canvas.width.float32, canvas.height.float32), + ) + discard window.requestAnimationFrame(drawFrame) + + discard window.requestAnimationFrame(drawFrame) + +when isMainModule: + main() diff --git a/src/figdraw/common/fontutils.nim b/src/figdraw/common/fontutils.nim index 9cf0d5a..786f829 100644 --- a/src/figdraw/common/fontutils.nim +++ b/src/figdraw/common/fontutils.nim @@ -1,486 +1,531 @@ -import std/[os, unicode, sequtils, tables, strutils, sets, hashes] -import std/isolation - -import pkg/vmath -import pkg/pixie -import pkg/pixie/fonts -import ../utils/logging - -import ./rchannels -import ./imgutils -import ./fonttypes -import ./shared - -type GlyphPosition* = ref object ## Represents a glyph position after typesetting. - fontId*: FontId - rune*: Rune - pos*: Vec2 # Where to draw the image character. - rect*: Rect - descent*: float32 - lineHeight*: float32 - -proc toSlices*[T: SomeInteger](a: openArray[(T, T)]): seq[Slice[T]] = - a.mapIt(it[0] .. it[1]) - -proc hash*(tp: Typeface): Hash = - var h = Hash(0) - h = h !& hash tp.filePath - result = !$h - -proc hash*(glyph: GlyphPosition): Hash {.inline.} = - result = hash((2344, glyph.fontId, glyph.rune)) - -proc getId*(typeface: Typeface): TypefaceId = - result = TypefaceId typeface.hash() - for i in 1 .. 100: - if result.int == 0: - result = TypefaceId(typeface.hash() !& hash(i)) - else: - break - doAssert result.int != 0, "Typeface hash results in invalid id" +when defined(js): + import ./fonttypes + import ./uimaths + + type TypeFaceKinds* = enum + TTF + OTF + SVG + + type Box* = Rect + + proc getTypefaceImpl*(name: string): FontId = + FontId(0) + + proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = + FontId(0) + + proc getLineHeightImpl*(font: UiFont): float32 = + 0.0 + + proc getTypesetImpl*( + box: Box, + spans: openArray[(UiFont, string)], + hAlign = Left, + vAlign = Top, + minContent = false, + wrap = true, + ): GlyphArrangement = + GlyphArrangement() + +else: + import std/[os, unicode, sequtils, tables, strutils, sets, hashes] + import std/isolation + + import pkg/vmath + import pkg/pixie + import pkg/pixie/fonts + import ../utils/logging + + import ./rchannels + import ./imgutils + import ./fonttypes + import ./shared + + type GlyphPosition* = ref object ## Represents a glyph position after typesetting. + fontId*: FontId + rune*: Rune + pos*: Vec2 # Where to draw the image character. + rect*: Rect + descent*: float32 + lineHeight*: float32 + + proc toSlices*[T: SomeInteger](a: openArray[(T, T)]): seq[Slice[T]] = + a.mapIt(it[0] .. it[1]) + + proc hash*(tp: Typeface): Hash = + var h = Hash(0) + h = h !& hash tp.filePath + result = !$h + + proc hash*(glyph: GlyphPosition): Hash {.inline.} = + result = hash((2344, glyph.fontId, glyph.rune)) + + proc getId*(typeface: Typeface): TypefaceId = + result = TypefaceId typeface.hash() + for i in 1 .. 100: + if result.int == 0: + result = TypefaceId(typeface.hash() !& hash(i)) + else: + break + doAssert result.int != 0, "Typeface hash results in invalid id" + + iterator glyphs*(arrangement: GlyphArrangement): GlyphPosition = + var idx = 0 + + block: + for i, (span, gfont) in zip(arrangement.spans, arrangement.fonts): + while idx < arrangement.runes.len(): + let + pos = arrangement.positions[idx] + rune = arrangement.runes[idx] + selection = arrangement.selectionRects[idx] + + let descent = gfont.lineHeight - gfont.descentAdj + + yield GlyphPosition( + fontId: gfont.fontId, + # fontSize: gfont.size, + rune: rune, + pos: pos, + rect: selection, + descent: descent, + lineHeight: gfont.lineHeight, + ) + + idx.inc() + if idx notin span: + break -iterator glyphs*(arrangement: GlyphArrangement): GlyphPosition = - var idx = 0 + var + typefaceTable*: Table[TypefaceId, Typeface] ## holds the table of parsed fonts + fontTable* {.threadvar.}: Table[FontId, pixie.Font] + + proc generateGlyphImage(arrangement: GlyphArrangement) = + ## returns Glyph's hash, will generate glyph if needed + ## + ## Font Glyphs are generated with Bottom vAlign and Center hAlign + ## this puts the glyphs in the right position + ## so that the renderer doesn't need to figure out adjustments + + for glyph in arrangement.glyphs(): + if unicode.isWhiteSpace(glyph.rune): + # echo "skipped:rune: ", glyph.rune, " ", glyph.rune.int + continue - block: - for i, (span, gfont) in zip(arrangement.spans, arrangement.fonts): - while idx < arrangement.runes.len(): + let hashFill = glyph.hash() + + if not hasImage(hashFill.ImageId): let - pos = arrangement.positions[idx] - rune = arrangement.runes[idx] - selection = arrangement.selectionRects[idx] - - let descent = gfont.lineHeight - gfont.descentAdj - - yield GlyphPosition( - fontId: gfont.fontId, - # fontSize: gfont.size, - rune: rune, - pos: pos, - rect: selection, - descent: descent, - lineHeight: gfont.lineHeight, - ) - - idx.inc() - if idx notin span: - break - -var - typefaceTable*: Table[TypefaceId, Typeface] ## holds the table of parsed fonts - fontTable* {.threadvar.}: Table[FontId, pixie.Font] - -proc generateGlyphImage(arrangement: GlyphArrangement) = - ## returns Glyph's hash, will generate glyph if needed - ## - ## Font Glyphs are generated with Bottom vAlign and Center hAlign - ## this puts the glyphs in the right position - ## so that the renderer doesn't need to figure out adjustments - - for glyph in arrangement.glyphs(): - if unicode.isWhiteSpace(glyph.rune): - # echo "skipped:rune: ", glyph.rune, " ", glyph.rune.int - continue - - let hashFill = glyph.hash() - - if not hasImage(hashFill.ImageId): - let - wh = glyph.rect.wh - fontId = glyph.fontId - font = fontTable[fontId] - text = $glyph.rune - arrangement = pixie.typeset( - @[newSpan(text, font)], - bounds = wh, - hAlign = CenterAlign, - vAlign = TopAlign, - wrap = false, - ) - let - snappedBounds = arrangement.computeBounds().snapToPixels() - - let - lh = font.defaultLineHeight() - bounds = rect(0, 0, snappedBounds.w + snappedBounds.x, lh) - - if bounds.w == 0 or bounds.h == 0: - echo "GEN IMG: ", glyph.rune, " wh: ", wh, " snapped: ", snappedBounds - continue + wh = glyph.rect.wh + fontId = glyph.fontId + font = fontTable[fontId] + text = $glyph.rune + arrangement = pixie.typeset( + @[newSpan(text, font)], + bounds = wh, + hAlign = CenterAlign, + vAlign = TopAlign, + wrap = false, + ) + let + snappedBounds = arrangement.computeBounds().snapToPixels() - try: - font.paint = parseHex"FFFFFF" - var image = newImage(bounds.w.int, bounds.h.int) - image.fillText(arrangement) - - # put into cache - loadImage(hashFill.ImageId, image) - except PixieError: - discard - -type TypeFaceKinds* = enum - TTF - OTF - SVG - -proc readTypefaceImpl( - name, data: string, kind: TypeFaceKinds -): Typeface {.raises: [PixieError].} = - ## Loads a typeface from a buffer - try: - result = - case kind - of TTF: - parseTtf(data) - of OTF: - parseOtf(data) - of SVG: - parseSvgFont(data) - except IOError as e: - raise newException(PixieError, e.msg, e) - - result.filePath = name - -proc getTypefaceImpl*(name: string): FontId = - ## loads a font from a file and adds it to the font index - - let - typefacePath = figDataDir() / name - typeface = readTypeface(typefacePath) - id = typeface.getId() - - doAssert id != 0 - if id in typefaceTable: - doAssert typefaceTable[id] == typeface - typefaceTable[id] = typeface - result = id - -proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = - ## loads a font from buffer and adds it to the font index - - let - typeface = readTypefaceImpl(name, data, kind) - id = typeface.getId() - - typefaceTable[id] = typeface - result = id - -proc convertFont*(font: UiFont): (FontId, Font) = - ## does the typesetting using pixie, then converts to Figuro's internal - ## types - - let - id = font.getId() - typeface = typefaceTable[font.typefaceId] - - if not fontTable.hasKey(id): - var pxfont = newFont(typeface) - pxfont.size = font.size.scaled - pxfont.typeface = typeface - pxfont.textCase = parseEnum[TextCase]($font.fontCase) - # copy rest of the fields with matching names - for pn, a in fieldPairs(pxfont[]): - for fn, b in fieldPairs(font): - when pn == fn: - a = b - if font.lineHeightOverride == -1.0'f32: - pxfont.lineHeight = font.lineHeightScale * pxfont.defaultLineHeight() - echo "PIXIE LH: ", pxfont.lineHeight - - fontTable[id] = pxfont - result = (id, pxfont) - else: - result = (id, fontTable[id]) - -proc getLineHeightImpl*(font: UiFont): float32 = - let (_, pf) = font.convertFont() - result = pf.lineHeight.descaled() - -proc calcMinMaxContent( - textLayout: GlyphArrangement -): tuple[maxSize, minSize: Vec2, bounding: Rect] = - ## estimate the maximum and minimum size of a given typesetting - - var longestWord: Slice[int] - var longestWordLen: float - - var words = 0 - var wordsHeight = 0.0 - var curr: Slice[int] - var currLen: float - var maxWidth: float - var rect: Rect = rect(float32.high, float32.high, 0, 0) - - # find longest word and count the number of words - # herein min content width is longest word - # herein max content height is a word on each line - var idx = 0 - for glyph in textLayout.glyphs(): - maxWidth += glyph.rect.w - rect.x = min(rect.x, glyph.rect.x) - rect.y = min(rect.y, glyph.rect.y) - rect.w = max(rect.w, glyph.rect.x + glyph.rect.w) - rect.h = max(rect.h, glyph.rect.y + glyph.rect.h) - - if glyph.rune.isWhiteSpace: - curr = idx + 1 .. idx - currLen = 0.0 + let + lh = font.defaultLineHeight() + bounds = rect(0, 0, snappedBounds.w + snappedBounds.x, lh) + + if bounds.w == 0 or bounds.h == 0: + echo "GEN IMG: ", glyph.rune, " wh: ", wh, " snapped: ", snappedBounds + continue + + try: + font.paint = parseHex"FFFFFF" + var image = newImage(bounds.w.int, bounds.h.int) + image.fillText(arrangement) + + # put into cache + loadImage(hashFill.ImageId, image) + except PixieError: + discard + + type TypeFaceKinds* = enum + TTF + OTF + SVG + + proc readTypefaceImpl( + name, data: string, kind: TypeFaceKinds + ): Typeface {.raises: [PixieError].} = + ## Loads a typeface from a buffer + try: + result = + case kind + of TTF: + parseTtf(data) + of OTF: + parseOtf(data) + of SVG: + parseSvgFont(data) + except IOError as e: + raise newException(PixieError, e.msg, e) + + result.filePath = name + + proc getTypefaceImpl*(name: string): FontId = + ## loads a font from a file and adds it to the font index + + let + typefacePath = figDataDir() / name + typeface = readTypeface(typefacePath) + id = typeface.getId() + + doAssert id != 0 + if id in typefaceTable: + doAssert typefaceTable[id] == typeface + typefaceTable[id] = typeface + result = id + + proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = + ## loads a font from buffer and adds it to the font index + + let + typeface = readTypefaceImpl(name, data, kind) + id = typeface.getId() + + typefaceTable[id] = typeface + result = id + + proc convertFont*(font: UiFont): (FontId, Font) = + ## does the typesetting using pixie, then converts to Figuro's internal + ## types + + let + id = font.getId() + typeface = typefaceTable[font.typefaceId] + + if not fontTable.hasKey(id): + var pxfont = newFont(typeface) + pxfont.size = font.size.scaled + pxfont.typeface = typeface + pxfont.textCase = parseEnum[TextCase]($font.fontCase) + # copy rest of the fields with matching names + for pn, a in fieldPairs(pxfont[]): + for fn, b in fieldPairs(font): + when pn == fn: + a = b + if font.lineHeightOverride == -1.0'f32: + pxfont.lineHeight = font.lineHeightScale * pxfont.defaultLineHeight() + echo "PIXIE LH: ", pxfont.lineHeight + + fontTable[id] = pxfont + result = (id, pxfont) else: - if curr.len() == 1: - words.inc - wordsHeight += glyph.lineHeight - curr.b = idx - currLen += glyph.rect.w - - if currLen > longestWordLen: - longestWord = curr - longestWordLen = currLen - - idx.inc() - - # find tallest font - var maxLine = 0.0 - for font in textLayout.fonts: - maxLine = max(maxLine, font.lineHeight) - - # set results - result.minSize.x = longestWordLen.descaled() - result.minSize.y = maxLine.descaled() - - result.maxSize.x = maxWidth.descaled() - result.maxSize.y = wordsHeight.descaled() - - result.bounding = rect.descaled() - -proc convertArrangement( - arrangement: Arrangement, - box: Rect, - uiSpans: openArray[(UiFont, string)], - hAlign: FontHorizontal, - vAlign: FontVertical, - gfonts: seq[GlyphFont], -): GlyphArrangement = - var - lines = newSeqOfCap[Slice[int]](arrangement.lines.len()) - spanSlices = newSeqOfCap[Slice[int]](arrangement.spans.len()) - selectionRects = newSeqOfCap[Rect](arrangement.selectionRects.len()) - for line in arrangement.lines: - lines.add line[0] .. line[1] - for span in arrangement.spans: - spanSlices.add span[0] .. span[1] - for rect in arrangement.selectionRects: - selectionRects.add rect - - result = GlyphArrangement( - contentHash: getContentHash(box.wh, uiSpans, hAlign, vAlign), - lines: lines, # arrangement.lines.toSlices(), - spans: spanSlices, # arrangement.spans.toSlices(), - fonts: gfonts, - runes: arrangement.runes, - positions: arrangement.positions, - selectionRects: selectionRects, - ) - -proc typeset*( - box: Rect, - uiSpans: openArray[(UiFont, string)], - hAlign = FontHorizontal.Left, - vAlign = FontVertical.Top, - minContent: bool, - wrap: bool, -): GlyphArrangement = - ## does the typesetting using pixie, then converts the typeseet results - ## into Figuro's own internal types - ## Primarily done for thread safety - threadEffects: - AppMainThread - - var - wh = box.scaled().wh - sz = uiSpans.mapIt(it[0].size.float) - minSz = sz.foldl(max(a, b), 0.0) - - var spans: seq[Span] - var pfs: seq[Font] - var gfonts: seq[GlyphFont] - for (uiFont, txt) in uiSpans: - let (_, pf) = uiFont.convertFont() - pfs.add(pf) - spans.add(newSpan(txt, pf)) - assert not pf.typeface.isNil - # There's gotta be a better way. Need to lookup the font formulas or equations or something - #let lhAdj = pf.lineHeight - #let lhAdj = max(pf.lineHeight - pf.size, 0.0) - let lhAdj = (pf.lineHeight - pf.size * pf.lineHeight / pf.defaultLineHeight()) / 2 - gfonts.add GlyphFont( - fontId: uiFont.getId(), lineHeight: pf.lineHeight, descentAdj: lhAdj + result = (id, fontTable[id]) + + proc getLineHeightImpl*(font: UiFont): float32 = + let (_, pf) = font.convertFont() + result = pf.lineHeight.descaled() + + proc calcMinMaxContent( + textLayout: GlyphArrangement + ): tuple[maxSize, minSize: Vec2, bounding: Rect] = + ## estimate the maximum and minimum size of a given typesetting + + var longestWord: Slice[int] + var longestWordLen: float + + var words = 0 + var wordsHeight = 0.0 + var curr: Slice[int] + var currLen: float + var maxWidth: float + var rect: Rect = rect(float32.high, float32.high, 0, 0) + + # find longest word and count the number of words + # herein min content width is longest word + # herein max content height is a word on each line + var idx = 0 + for glyph in textLayout.glyphs(): + maxWidth += glyph.rect.w + rect.x = min(rect.x, glyph.rect.x) + rect.y = min(rect.y, glyph.rect.y) + rect.w = max(rect.w, glyph.rect.x + glyph.rect.w) + rect.h = max(rect.h, glyph.rect.y + glyph.rect.h) + + if glyph.rune.isWhiteSpace: + curr = idx + 1 .. idx + currLen = 0.0 + else: + if curr.len() == 1: + words.inc + wordsHeight += glyph.lineHeight + curr.b = idx + currLen += glyph.rect.w + + if currLen > longestWordLen: + longestWord = curr + longestWordLen = currLen + + idx.inc() + + # find tallest font + var maxLine = 0.0 + for font in textLayout.fonts: + maxLine = max(maxLine, font.lineHeight) + + # set results + result.minSize.x = longestWordLen.descaled() + result.minSize.y = maxLine.descaled() + + result.maxSize.x = maxWidth.descaled() + result.maxSize.y = wordsHeight.descaled() + + result.bounding = rect.descaled() + + proc convertArrangement( + arrangement: Arrangement, + box: Rect, + uiSpans: openArray[(UiFont, string)], + hAlign: FontHorizontal, + vAlign: FontVertical, + gfonts: seq[GlyphFont], + ): GlyphArrangement = + var + lines = newSeqOfCap[Slice[int]](arrangement.lines.len()) + spanSlices = newSeqOfCap[Slice[int]](arrangement.spans.len()) + selectionRects = newSeqOfCap[Rect](arrangement.selectionRects.len()) + for line in arrangement.lines: + lines.add line[0] .. line[1] + for span in arrangement.spans: + spanSlices.add span[0] .. span[1] + for rect in arrangement.selectionRects: + selectionRects.add rect + + result = GlyphArrangement( + contentHash: getContentHash(box.wh, uiSpans, hAlign, vAlign), + lines: lines, # arrangement.lines.toSlices(), + spans: spanSlices, # arrangement.spans.toSlices(), + fonts: gfonts, + runes: arrangement.runes, + positions: arrangement.positions, + selectionRects: selectionRects, ) - var ha: HorizontalAlignment - case hAlign - of Left: - ha = LeftAlign - of Center: - ha = CenterAlign - of Right: - ha = RightAlign - - var va: VerticalAlignment - case vAlign - of Top: - va = TopAlign - of Middle: - va = MiddleAlign - of Bottom: - va = BottomAlign - - let arrangement = - pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) - result = convertArrangement(arrangement, box, uiSpans, hAlign, vAlign, gfonts) - - let content = result.calcMinMaxContent() - result.minSize = content.minSize - result.maxSize = content.maxSize - result.bounding = content.bounding - - if minContent: - ## calcaulate min width of content - var wh = wh - wh.y = result.maxSize.y.scaled() - let arr = pixie.typeset( - spans, bounds = wh, hAlign = LeftAlign, vAlign = TopAlign, wrap = wrap - ) - let minResult = convertArrangement(arr, box, uiSpans, hAlign, vAlign, gfonts) - - let minContent = minResult.calcMinMaxContent() - trace "minContent:", - boxWh = box.wh, - wh = wh, - minSize = minContent.minSize, - maxSize = minContent.maxSize, - bounding = minContent.bounding, - boundH = result.bounding.h - - if minContent.bounding.h > result.bounding.h: - let wh = vec2(wh.x, minContent.bounding.h.scaled()) - let minAdjusted = - pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) - result = convertArrangement(minAdjusted, box, uiSpans, hAlign, vAlign, gfonts) - let contentAdjusted = result.calcMinMaxContent() - result.minSize = contentAdjusted.minSize - result.maxSize = contentAdjusted.maxSize - result.bounding = contentAdjusted.bounding - trace "minContent:adjusted", + proc typeset*( + box: Rect, + uiSpans: openArray[(UiFont, string)], + hAlign = FontHorizontal.Left, + vAlign = FontVertical.Top, + minContent: bool, + wrap: bool, + ): GlyphArrangement = + ## does the typesetting using pixie, then converts the typeseet results + ## into Figuro's own internal types + ## Primarily done for thread safety + threadEffects: + AppMainThread + + var + wh = box.scaled().wh + sz = uiSpans.mapIt(it[0].size.float) + minSz = sz.foldl(max(a, b), 0.0) + + var spans: seq[Span] + var pfs: seq[Font] + var gfonts: seq[GlyphFont] + for (uiFont, txt) in uiSpans: + let (_, pf) = uiFont.convertFont() + pfs.add(pf) + spans.add(newSpan(txt, pf)) + assert not pf.typeface.isNil + # There's gotta be a better way. Need to lookup the font formulas or equations or something + #let lhAdj = pf.lineHeight + #let lhAdj = max(pf.lineHeight - pf.size, 0.0) + let lhAdj = (pf.lineHeight - pf.size * pf.lineHeight / + pf.defaultLineHeight()) / 2 + gfonts.add GlyphFont( + fontId: uiFont.getId(), lineHeight: pf.lineHeight, descentAdj: lhAdj + ) + + var ha: HorizontalAlignment + case hAlign + of Left: + ha = LeftAlign + of Center: + ha = CenterAlign + of Right: + ha = RightAlign + + var va: VerticalAlignment + case vAlign + of Top: + va = TopAlign + of Middle: + va = MiddleAlign + of Bottom: + va = BottomAlign + + let arrangement = + pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) + result = convertArrangement(arrangement, box, uiSpans, hAlign, vAlign, gfonts) + + let content = result.calcMinMaxContent() + result.minSize = content.minSize + result.maxSize = content.maxSize + result.bounding = content.bounding + + if minContent: + ## calcaulate min width of content + var wh = wh + wh.y = result.maxSize.y.scaled() + let arr = pixie.typeset( + spans, bounds = wh, hAlign = LeftAlign, vAlign = TopAlign, wrap = wrap + ) + let minResult = convertArrangement(arr, box, uiSpans, hAlign, vAlign, gfonts) + + let minContent = minResult.calcMinMaxContent() + trace "minContent:", boxWh = box.wh, wh = wh, - wrap = wrap, - minSize = result.minSize, - maxSize = result.maxSize, - bounding = result.bounding - - result.minSize.y = result.bounding.h - else: - result.minSize.y = max(result.minSize.y, result.bounding.h) - - let maxLineHeight = max(sz) - result.minSize += vec2(maxLineHeight / 2, 0) - result.maxSize += vec2(maxLineHeight / 2, 0) - result.bounding = result.bounding + rect(0, 0, 0, maxLineHeight / 2) - # debug "getTypesetImpl:post:", boxWh= box.wh, wh= wh, contentHash = getContentHash(box.wh, uiSpans, hAlign, vAlign), - # minSize = result.minSize, maxSize = result.maxSize, bounding = result.bounding - - result.generateGlyphImage() - # echo "font: " - # print arrangement.fonts[0].size - # print arrangement.fonts[0].lineHeight - # echo "arrangement: " - # print result - -proc glyphFontFor(uiFont: UiFont): tuple[id: FontId, font: Font, - glyph: GlyphFont] = - let (fontId, pf) = uiFont.convertFont() - let defaultLineHeight = pf.defaultLineHeight() - let lineHeight = - if pf.lineHeight >= 0: - pf.lineHeight - else: - defaultLineHeight - let lhAdj = - if defaultLineHeight > 0: - (lineHeight - pf.size * lineHeight / defaultLineHeight) / 2 - else: - 0.0'f32 - result = ( - id: fontId, - font: pf, - glyph: GlyphFont(fontId: fontId, lineHeight: lineHeight, descentAdj: lhAdj), - ) - -proc placeGlyphs*( - font: UiFont, - glyphs: openArray[(Rune, Vec2)], - origin: GlyphOrigin = GlyphTopLeft, -): GlyphArrangement = - ## Builds a glyph arrangement using explicit positions for each glyph. - ## `origin` controls whether positions are the glyph's top-left or baseline. - threadEffects: - AppMainThread - - result = GlyphArrangement() - if glyphs.len == 0: - return - - let fontInfo = glyphFontFor(font) - let cachedFont = (font: fontInfo.font, glyph: fontInfo.glyph) - - var - runes = newSeqOfCap[Rune](glyphs.len) - positions = newSeqOfCap[Vec2](glyphs.len) - selectionRects = newSeqOfCap[Rect](glyphs.len) - contentHash = Hash(0) - - for (rune, pos) in glyphs: - - let scaledPos = pos.scaled() - let descent = cachedFont.glyph.lineHeight - cachedFont.glyph.descentAdj - var baselinePos = scaledPos - if origin == GlyphTopLeft: - baselinePos.y = scaledPos.y + descent - - runes.add(rune) - positions.add(baselinePos) - - let drawPos = vec2(baselinePos.x, baselinePos.y - descent) - let advance = cachedFont.font.typeface.getAdvance(rune) * - cachedFont.font.scale - selectionRects.add( - rect(drawPos.x, drawPos.y, advance, cachedFont.glyph.lineHeight) + minSize = minContent.minSize, + maxSize = minContent.maxSize, + bounding = minContent.bounding, + boundH = result.bounding.h + + if minContent.bounding.h > result.bounding.h: + let wh = vec2(wh.x, minContent.bounding.h.scaled()) + let minAdjusted = + pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) + result = convertArrangement(minAdjusted, box, uiSpans, hAlign, vAlign, gfonts) + let contentAdjusted = result.calcMinMaxContent() + result.minSize = contentAdjusted.minSize + result.maxSize = contentAdjusted.maxSize + result.bounding = contentAdjusted.bounding + trace "minContent:adjusted", + boxWh = box.wh, + wh = wh, + wrap = wrap, + minSize = result.minSize, + maxSize = result.maxSize, + bounding = result.bounding + + result.minSize.y = result.bounding.h + else: + result.minSize.y = max(result.minSize.y, result.bounding.h) + + let maxLineHeight = max(sz) + result.minSize += vec2(maxLineHeight / 2, 0) + result.maxSize += vec2(maxLineHeight / 2, 0) + result.bounding = result.bounding + rect(0, 0, 0, maxLineHeight / 2) + # debug "getTypesetImpl:post:", boxWh= box.wh, wh= wh, contentHash = getContentHash(box.wh, uiSpans, hAlign, vAlign), + # minSize = result.minSize, maxSize = result.maxSize, bounding = result.bounding + + result.generateGlyphImage() + # echo "font: " + # print arrangement.fonts[0].size + # print arrangement.fonts[0].lineHeight + # echo "arrangement: " + # print result + + proc glyphFontFor(uiFont: UiFont): tuple[id: FontId, font: Font, + glyph: GlyphFont] = + let (fontId, pf) = uiFont.convertFont() + let defaultLineHeight = pf.defaultLineHeight() + let lineHeight = + if pf.lineHeight >= 0: + pf.lineHeight + else: + defaultLineHeight + let lhAdj = + if defaultLineHeight > 0: + (lineHeight - pf.size * lineHeight / defaultLineHeight) / 2 + else: + 0.0'f32 + result = ( + id: fontId, + font: pf, + glyph: GlyphFont(fontId: fontId, lineHeight: lineHeight, + descentAdj: lhAdj), ) - contentHash = contentHash !& hash((font.getId(), rune, pos.x, pos.y, origin)) - - result.lines = @[0 .. glyphs.len - 1] - result.spans = @[0 .. glyphs.len - 1] - result.fonts = @[cachedFont.glyph] - result.runes = runes - result.positions = positions - result.selectionRects = selectionRects - result.contentHash = !$contentHash - - var - minX = float32.high - minY = float32.high - maxX = -float32.high - maxY = -float32.high - for rect in selectionRects: - minX = min(minX, rect.x) - minY = min(minY, rect.y) - maxX = max(maxX, rect.x + rect.w) - maxY = max(maxY, rect.y + rect.h) - if selectionRects.len > 0: - let boundingScaled = rect(minX, minY, maxX - minX, maxY - minY) - result.bounding = boundingScaled.descaled() - result.minSize = result.bounding.wh - result.maxSize = result.bounding.wh - - result.generateGlyphImage() + proc placeGlyphs*( + font: UiFont, + glyphs: openArray[(Rune, Vec2)], + origin: GlyphOrigin = GlyphTopLeft, + ): GlyphArrangement = + ## Builds a glyph arrangement using explicit positions for each glyph. + ## `origin` controls whether positions are the glyph's top-left or baseline. + threadEffects: + AppMainThread + + result = GlyphArrangement() + if glyphs.len == 0: + return + + let fontInfo = glyphFontFor(font) + let cachedFont = (font: fontInfo.font, glyph: fontInfo.glyph) + + var + runes = newSeqOfCap[Rune](glyphs.len) + positions = newSeqOfCap[Vec2](glyphs.len) + selectionRects = newSeqOfCap[Rect](glyphs.len) + contentHash = Hash(0) + + for (rune, pos) in glyphs: + + let scaledPos = pos.scaled() + let descent = cachedFont.glyph.lineHeight - cachedFont.glyph.descentAdj + var baselinePos = scaledPos + if origin == GlyphTopLeft: + baselinePos.y = scaledPos.y + descent + + runes.add(rune) + positions.add(baselinePos) + + let drawPos = vec2(baselinePos.x, baselinePos.y - descent) + let advance = cachedFont.font.typeface.getAdvance(rune) * + cachedFont.font.scale + selectionRects.add( + rect(drawPos.x, drawPos.y, advance, cachedFont.glyph.lineHeight) + ) + + contentHash = contentHash !& hash((font.getId(), rune, pos.x, pos.y, origin)) + + result.lines = @[0 .. glyphs.len - 1] + result.spans = @[0 .. glyphs.len - 1] + result.fonts = @[cachedFont.glyph] + result.runes = runes + result.positions = positions + result.selectionRects = selectionRects + result.contentHash = !$contentHash + + var + minX = float32.high + minY = float32.high + maxX = -float32.high + maxY = -float32.high + for rect in selectionRects: + minX = min(minX, rect.x) + minY = min(minY, rect.y) + maxX = max(maxX, rect.x + rect.w) + maxY = max(maxY, rect.y + rect.h) + if selectionRects.len > 0: + let boundingScaled = rect(minX, minY, maxX - minX, maxY - minY) + result.bounding = boundingScaled.descaled() + result.minSize = result.bounding.wh + result.maxSize = result.bounding.wh + + result.generateGlyphImage() + + type Box* = Rect + + proc getTypesetImpl*( + box: Box, + spans: openArray[(UiFont, string)], + hAlign = Left, + vAlign = Top, + minContent = false, + wrap = true, + ): GlyphArrangement = + typeset(box, spans, hAlign, vAlign, minContent, wrap) diff --git a/src/figdraw/common/fontutils_js.nim b/src/figdraw/common/fontutils_js.nim deleted file mode 100644 index 6af8239..0000000 --- a/src/figdraw/common/fontutils_js.nim +++ /dev/null @@ -1,28 +0,0 @@ -import fonttypes -import uimaths - -type TypeFaceKinds* = enum - TTF - OTF - SVG - -type Box* = Rect - -proc getTypefaceImpl*(name: string): FontId = - FontId(0) - -proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = - FontId(0) - -proc getLineHeightImpl*(font: UiFont): float32 = - 0.0 - -proc getTypesetImpl*( - box: Box, - spans: openArray[(UiFont, string)], - hAlign = Left, - vAlign = Top, - minContent = false, - wrap = true, -): GlyphArrangement = - GlyphArrangement() diff --git a/src/figdraw/commons.nim b/src/figdraw/commons.nim index db151a9..6992bb8 100644 --- a/src/figdraw/commons.nim +++ b/src/figdraw/commons.nim @@ -6,11 +6,10 @@ else: import common/rchannels import common/transfer import common/appframes +import common/fontutils when defined(js): - import common/fontutils_js as fontutils import common/imgutils_js as imgutils else: - import common/fontutils import common/imgutils export shared, uimaths, rchannels From 9b301b65017d888a4c637b8c09dbd90202943015 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:34:24 -0700 Subject: [PATCH 05/10] add webgl --- examples/webgl_renderlist_100.nim | 54 +++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/examples/webgl_renderlist_100.nim b/examples/webgl_renderlist_100.nim index a89ef9f..f1b1ab0 100644 --- a/examples/webgl_renderlist_100.nim +++ b/examples/webgl_renderlist_100.nim @@ -1,8 +1,7 @@ when not defined(js) and not defined(nimsuggest): {.fatal: "This example requires the Nim JS backend (nim js).".} -import std/[dom, jsconsole, jsffi] -import chroma +import std/[dom, jsconsole, jsffi, strutils] import figdraw/commons import figdraw/fignodes @@ -22,12 +21,49 @@ proc main() = app.pixelScale = 1.0 let canvas = webgl.asCanvas(document.createElement("canvas")) - document.body.appendChild(canvas) document.body.style.margin = "0" document.body.style.overflow = "hidden" document.body.style.background = "#0c0f16" + document.body.style.height = "100%" + + let root = document.createElement("div") + root.style.position = "relative" + root.style.width = "100%" + root.style.height = "100%" + root.style.overflow = "hidden" + document.body.appendChild(root) + canvas.style.display = "block" + canvas.style.position = "absolute" + canvas.style.left = "0" + canvas.style.top = "0" + root.appendChild(canvas) + + let textLayer = document.createElement("div") + textLayer.style.position = "absolute" + textLayer.style.left = "0" + textLayer.style.top = "0" + textLayer.style.width = "100%" + textLayer.style.height = "100%" + textLayer.style.pointerEvents = "none" + textLayer.style.zIndex = "2" + root.appendChild(textLayer) + + let fpsNode = document.createElement("div") + fpsNode.style.position = "absolute" + fpsNode.style.left = "12px" + fpsNode.style.top = "10px" + fpsNode.style.padding = "4px 6px" + fpsNode.style.borderRadius = "6px" + fpsNode.style.color = "#cfe2ff" + fpsNode.style.background = "rgba(12, 15, 22, 0.55)" + fpsNode.style.fontFamily = + "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" + fpsNode.style.fontSize = "12px" + fpsNode.style.letterSpacing = "0.3px" + fpsNode.textContent = "FPS: --" + textLayer.appendChild(fpsNode) let gl = cast[glapi.WebGL2RenderingContext](canvas.getContext("webgl2")) if gl.isNull or gl.isUndefined: @@ -61,6 +97,9 @@ proc main() = result = vec2(cssWidth.float32, cssHeight.float32) + var fpsLastTime = 0.0 + var fpsFrames = 0 + proc drawFrame(time: float) = inc globalFrame let cssSize = updateCanvas() @@ -69,6 +108,15 @@ proc main() = renders, vec2(canvas.width.float32, canvas.height.float32), ) + inc fpsFrames + if fpsLastTime == 0.0: + fpsLastTime = time + let elapsed = time - fpsLastTime + if elapsed >= 500.0: + let fps = fpsFrames.float * 1000.0 / elapsed + fpsNode.textContent = cstring("FPS: " & formatFloat(fps, ffDecimal, 1)) + fpsFrames = 0 + fpsLastTime = time discard window.requestAnimationFrame(drawFrame) discard window.requestAnimationFrame(drawFrame) From 68fe5cd28da6a308ea8ec1ee284281d217cc13e6 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:41:33 -0700 Subject: [PATCH 06/10] add webgl --- src/figdraw/common/fontutils.nim | 995 +++++++++++++--------------- src/figdraw/common/fontutils_js.nim | 28 + src/figdraw/commons.nim | 3 +- 3 files changed, 505 insertions(+), 521 deletions(-) create mode 100644 src/figdraw/common/fontutils_js.nim diff --git a/src/figdraw/common/fontutils.nim b/src/figdraw/common/fontutils.nim index 786f829..9cf0d5a 100644 --- a/src/figdraw/common/fontutils.nim +++ b/src/figdraw/common/fontutils.nim @@ -1,531 +1,486 @@ -when defined(js): - import ./fonttypes - import ./uimaths - - type TypeFaceKinds* = enum - TTF - OTF - SVG - - type Box* = Rect - - proc getTypefaceImpl*(name: string): FontId = - FontId(0) - - proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = - FontId(0) - - proc getLineHeightImpl*(font: UiFont): float32 = - 0.0 - - proc getTypesetImpl*( - box: Box, - spans: openArray[(UiFont, string)], - hAlign = Left, - vAlign = Top, - minContent = false, - wrap = true, - ): GlyphArrangement = - GlyphArrangement() - -else: - import std/[os, unicode, sequtils, tables, strutils, sets, hashes] - import std/isolation - - import pkg/vmath - import pkg/pixie - import pkg/pixie/fonts - import ../utils/logging - - import ./rchannels - import ./imgutils - import ./fonttypes - import ./shared - - type GlyphPosition* = ref object ## Represents a glyph position after typesetting. - fontId*: FontId - rune*: Rune - pos*: Vec2 # Where to draw the image character. - rect*: Rect - descent*: float32 - lineHeight*: float32 - - proc toSlices*[T: SomeInteger](a: openArray[(T, T)]): seq[Slice[T]] = - a.mapIt(it[0] .. it[1]) - - proc hash*(tp: Typeface): Hash = - var h = Hash(0) - h = h !& hash tp.filePath - result = !$h - - proc hash*(glyph: GlyphPosition): Hash {.inline.} = - result = hash((2344, glyph.fontId, glyph.rune)) - - proc getId*(typeface: Typeface): TypefaceId = - result = TypefaceId typeface.hash() - for i in 1 .. 100: - if result.int == 0: - result = TypefaceId(typeface.hash() !& hash(i)) - else: - break - doAssert result.int != 0, "Typeface hash results in invalid id" - - iterator glyphs*(arrangement: GlyphArrangement): GlyphPosition = - var idx = 0 - - block: - for i, (span, gfont) in zip(arrangement.spans, arrangement.fonts): - while idx < arrangement.runes.len(): - let - pos = arrangement.positions[idx] - rune = arrangement.runes[idx] - selection = arrangement.selectionRects[idx] - - let descent = gfont.lineHeight - gfont.descentAdj - - yield GlyphPosition( - fontId: gfont.fontId, - # fontSize: gfont.size, - rune: rune, - pos: pos, - rect: selection, - descent: descent, - lineHeight: gfont.lineHeight, - ) - - idx.inc() - if idx notin span: - break - - var - typefaceTable*: Table[TypefaceId, Typeface] ## holds the table of parsed fonts - fontTable* {.threadvar.}: Table[FontId, pixie.Font] - - proc generateGlyphImage(arrangement: GlyphArrangement) = - ## returns Glyph's hash, will generate glyph if needed - ## - ## Font Glyphs are generated with Bottom vAlign and Center hAlign - ## this puts the glyphs in the right position - ## so that the renderer doesn't need to figure out adjustments - - for glyph in arrangement.glyphs(): - if unicode.isWhiteSpace(glyph.rune): - # echo "skipped:rune: ", glyph.rune, " ", glyph.rune.int - continue +import std/[os, unicode, sequtils, tables, strutils, sets, hashes] +import std/isolation + +import pkg/vmath +import pkg/pixie +import pkg/pixie/fonts +import ../utils/logging + +import ./rchannels +import ./imgutils +import ./fonttypes +import ./shared + +type GlyphPosition* = ref object ## Represents a glyph position after typesetting. + fontId*: FontId + rune*: Rune + pos*: Vec2 # Where to draw the image character. + rect*: Rect + descent*: float32 + lineHeight*: float32 + +proc toSlices*[T: SomeInteger](a: openArray[(T, T)]): seq[Slice[T]] = + a.mapIt(it[0] .. it[1]) + +proc hash*(tp: Typeface): Hash = + var h = Hash(0) + h = h !& hash tp.filePath + result = !$h + +proc hash*(glyph: GlyphPosition): Hash {.inline.} = + result = hash((2344, glyph.fontId, glyph.rune)) + +proc getId*(typeface: Typeface): TypefaceId = + result = TypefaceId typeface.hash() + for i in 1 .. 100: + if result.int == 0: + result = TypefaceId(typeface.hash() !& hash(i)) + else: + break + doAssert result.int != 0, "Typeface hash results in invalid id" - let hashFill = glyph.hash() +iterator glyphs*(arrangement: GlyphArrangement): GlyphPosition = + var idx = 0 - if not hasImage(hashFill.ImageId): - let - wh = glyph.rect.wh - fontId = glyph.fontId - font = fontTable[fontId] - text = $glyph.rune - arrangement = pixie.typeset( - @[newSpan(text, font)], - bounds = wh, - hAlign = CenterAlign, - vAlign = TopAlign, - wrap = false, - ) + block: + for i, (span, gfont) in zip(arrangement.spans, arrangement.fonts): + while idx < arrangement.runes.len(): let - snappedBounds = arrangement.computeBounds().snapToPixels() + pos = arrangement.positions[idx] + rune = arrangement.runes[idx] + selection = arrangement.selectionRects[idx] + + let descent = gfont.lineHeight - gfont.descentAdj + + yield GlyphPosition( + fontId: gfont.fontId, + # fontSize: gfont.size, + rune: rune, + pos: pos, + rect: selection, + descent: descent, + lineHeight: gfont.lineHeight, + ) + + idx.inc() + if idx notin span: + break + +var + typefaceTable*: Table[TypefaceId, Typeface] ## holds the table of parsed fonts + fontTable* {.threadvar.}: Table[FontId, pixie.Font] + +proc generateGlyphImage(arrangement: GlyphArrangement) = + ## returns Glyph's hash, will generate glyph if needed + ## + ## Font Glyphs are generated with Bottom vAlign and Center hAlign + ## this puts the glyphs in the right position + ## so that the renderer doesn't need to figure out adjustments + + for glyph in arrangement.glyphs(): + if unicode.isWhiteSpace(glyph.rune): + # echo "skipped:rune: ", glyph.rune, " ", glyph.rune.int + continue + + let hashFill = glyph.hash() + + if not hasImage(hashFill.ImageId): + let + wh = glyph.rect.wh + fontId = glyph.fontId + font = fontTable[fontId] + text = $glyph.rune + arrangement = pixie.typeset( + @[newSpan(text, font)], + bounds = wh, + hAlign = CenterAlign, + vAlign = TopAlign, + wrap = false, + ) + let + snappedBounds = arrangement.computeBounds().snapToPixels() + + let + lh = font.defaultLineHeight() + bounds = rect(0, 0, snappedBounds.w + snappedBounds.x, lh) + + if bounds.w == 0 or bounds.h == 0: + echo "GEN IMG: ", glyph.rune, " wh: ", wh, " snapped: ", snappedBounds + continue - let - lh = font.defaultLineHeight() - bounds = rect(0, 0, snappedBounds.w + snappedBounds.x, lh) - - if bounds.w == 0 or bounds.h == 0: - echo "GEN IMG: ", glyph.rune, " wh: ", wh, " snapped: ", snappedBounds - continue - - try: - font.paint = parseHex"FFFFFF" - var image = newImage(bounds.w.int, bounds.h.int) - image.fillText(arrangement) - - # put into cache - loadImage(hashFill.ImageId, image) - except PixieError: - discard - - type TypeFaceKinds* = enum - TTF - OTF - SVG - - proc readTypefaceImpl( - name, data: string, kind: TypeFaceKinds - ): Typeface {.raises: [PixieError].} = - ## Loads a typeface from a buffer - try: - result = - case kind - of TTF: - parseTtf(data) - of OTF: - parseOtf(data) - of SVG: - parseSvgFont(data) - except IOError as e: - raise newException(PixieError, e.msg, e) - - result.filePath = name - - proc getTypefaceImpl*(name: string): FontId = - ## loads a font from a file and adds it to the font index - - let - typefacePath = figDataDir() / name - typeface = readTypeface(typefacePath) - id = typeface.getId() - - doAssert id != 0 - if id in typefaceTable: - doAssert typefaceTable[id] == typeface - typefaceTable[id] = typeface - result = id - - proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = - ## loads a font from buffer and adds it to the font index - - let - typeface = readTypefaceImpl(name, data, kind) - id = typeface.getId() - - typefaceTable[id] = typeface - result = id - - proc convertFont*(font: UiFont): (FontId, Font) = - ## does the typesetting using pixie, then converts to Figuro's internal - ## types - - let - id = font.getId() - typeface = typefaceTable[font.typefaceId] - - if not fontTable.hasKey(id): - var pxfont = newFont(typeface) - pxfont.size = font.size.scaled - pxfont.typeface = typeface - pxfont.textCase = parseEnum[TextCase]($font.fontCase) - # copy rest of the fields with matching names - for pn, a in fieldPairs(pxfont[]): - for fn, b in fieldPairs(font): - when pn == fn: - a = b - if font.lineHeightOverride == -1.0'f32: - pxfont.lineHeight = font.lineHeightScale * pxfont.defaultLineHeight() - echo "PIXIE LH: ", pxfont.lineHeight - - fontTable[id] = pxfont - result = (id, pxfont) + try: + font.paint = parseHex"FFFFFF" + var image = newImage(bounds.w.int, bounds.h.int) + image.fillText(arrangement) + + # put into cache + loadImage(hashFill.ImageId, image) + except PixieError: + discard + +type TypeFaceKinds* = enum + TTF + OTF + SVG + +proc readTypefaceImpl( + name, data: string, kind: TypeFaceKinds +): Typeface {.raises: [PixieError].} = + ## Loads a typeface from a buffer + try: + result = + case kind + of TTF: + parseTtf(data) + of OTF: + parseOtf(data) + of SVG: + parseSvgFont(data) + except IOError as e: + raise newException(PixieError, e.msg, e) + + result.filePath = name + +proc getTypefaceImpl*(name: string): FontId = + ## loads a font from a file and adds it to the font index + + let + typefacePath = figDataDir() / name + typeface = readTypeface(typefacePath) + id = typeface.getId() + + doAssert id != 0 + if id in typefaceTable: + doAssert typefaceTable[id] == typeface + typefaceTable[id] = typeface + result = id + +proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = + ## loads a font from buffer and adds it to the font index + + let + typeface = readTypefaceImpl(name, data, kind) + id = typeface.getId() + + typefaceTable[id] = typeface + result = id + +proc convertFont*(font: UiFont): (FontId, Font) = + ## does the typesetting using pixie, then converts to Figuro's internal + ## types + + let + id = font.getId() + typeface = typefaceTable[font.typefaceId] + + if not fontTable.hasKey(id): + var pxfont = newFont(typeface) + pxfont.size = font.size.scaled + pxfont.typeface = typeface + pxfont.textCase = parseEnum[TextCase]($font.fontCase) + # copy rest of the fields with matching names + for pn, a in fieldPairs(pxfont[]): + for fn, b in fieldPairs(font): + when pn == fn: + a = b + if font.lineHeightOverride == -1.0'f32: + pxfont.lineHeight = font.lineHeightScale * pxfont.defaultLineHeight() + echo "PIXIE LH: ", pxfont.lineHeight + + fontTable[id] = pxfont + result = (id, pxfont) + else: + result = (id, fontTable[id]) + +proc getLineHeightImpl*(font: UiFont): float32 = + let (_, pf) = font.convertFont() + result = pf.lineHeight.descaled() + +proc calcMinMaxContent( + textLayout: GlyphArrangement +): tuple[maxSize, minSize: Vec2, bounding: Rect] = + ## estimate the maximum and minimum size of a given typesetting + + var longestWord: Slice[int] + var longestWordLen: float + + var words = 0 + var wordsHeight = 0.0 + var curr: Slice[int] + var currLen: float + var maxWidth: float + var rect: Rect = rect(float32.high, float32.high, 0, 0) + + # find longest word and count the number of words + # herein min content width is longest word + # herein max content height is a word on each line + var idx = 0 + for glyph in textLayout.glyphs(): + maxWidth += glyph.rect.w + rect.x = min(rect.x, glyph.rect.x) + rect.y = min(rect.y, glyph.rect.y) + rect.w = max(rect.w, glyph.rect.x + glyph.rect.w) + rect.h = max(rect.h, glyph.rect.y + glyph.rect.h) + + if glyph.rune.isWhiteSpace: + curr = idx + 1 .. idx + currLen = 0.0 else: - result = (id, fontTable[id]) - - proc getLineHeightImpl*(font: UiFont): float32 = - let (_, pf) = font.convertFont() - result = pf.lineHeight.descaled() - - proc calcMinMaxContent( - textLayout: GlyphArrangement - ): tuple[maxSize, minSize: Vec2, bounding: Rect] = - ## estimate the maximum and minimum size of a given typesetting - - var longestWord: Slice[int] - var longestWordLen: float - - var words = 0 - var wordsHeight = 0.0 - var curr: Slice[int] - var currLen: float - var maxWidth: float - var rect: Rect = rect(float32.high, float32.high, 0, 0) - - # find longest word and count the number of words - # herein min content width is longest word - # herein max content height is a word on each line - var idx = 0 - for glyph in textLayout.glyphs(): - maxWidth += glyph.rect.w - rect.x = min(rect.x, glyph.rect.x) - rect.y = min(rect.y, glyph.rect.y) - rect.w = max(rect.w, glyph.rect.x + glyph.rect.w) - rect.h = max(rect.h, glyph.rect.y + glyph.rect.h) - - if glyph.rune.isWhiteSpace: - curr = idx + 1 .. idx - currLen = 0.0 - else: - if curr.len() == 1: - words.inc - wordsHeight += glyph.lineHeight - curr.b = idx - currLen += glyph.rect.w - - if currLen > longestWordLen: - longestWord = curr - longestWordLen = currLen - - idx.inc() - - # find tallest font - var maxLine = 0.0 - for font in textLayout.fonts: - maxLine = max(maxLine, font.lineHeight) - - # set results - result.minSize.x = longestWordLen.descaled() - result.minSize.y = maxLine.descaled() - - result.maxSize.x = maxWidth.descaled() - result.maxSize.y = wordsHeight.descaled() - - result.bounding = rect.descaled() - - proc convertArrangement( - arrangement: Arrangement, - box: Rect, - uiSpans: openArray[(UiFont, string)], - hAlign: FontHorizontal, - vAlign: FontVertical, - gfonts: seq[GlyphFont], - ): GlyphArrangement = - var - lines = newSeqOfCap[Slice[int]](arrangement.lines.len()) - spanSlices = newSeqOfCap[Slice[int]](arrangement.spans.len()) - selectionRects = newSeqOfCap[Rect](arrangement.selectionRects.len()) - for line in arrangement.lines: - lines.add line[0] .. line[1] - for span in arrangement.spans: - spanSlices.add span[0] .. span[1] - for rect in arrangement.selectionRects: - selectionRects.add rect - - result = GlyphArrangement( - contentHash: getContentHash(box.wh, uiSpans, hAlign, vAlign), - lines: lines, # arrangement.lines.toSlices(), - spans: spanSlices, # arrangement.spans.toSlices(), - fonts: gfonts, - runes: arrangement.runes, - positions: arrangement.positions, - selectionRects: selectionRects, + if curr.len() == 1: + words.inc + wordsHeight += glyph.lineHeight + curr.b = idx + currLen += glyph.rect.w + + if currLen > longestWordLen: + longestWord = curr + longestWordLen = currLen + + idx.inc() + + # find tallest font + var maxLine = 0.0 + for font in textLayout.fonts: + maxLine = max(maxLine, font.lineHeight) + + # set results + result.minSize.x = longestWordLen.descaled() + result.minSize.y = maxLine.descaled() + + result.maxSize.x = maxWidth.descaled() + result.maxSize.y = wordsHeight.descaled() + + result.bounding = rect.descaled() + +proc convertArrangement( + arrangement: Arrangement, + box: Rect, + uiSpans: openArray[(UiFont, string)], + hAlign: FontHorizontal, + vAlign: FontVertical, + gfonts: seq[GlyphFont], +): GlyphArrangement = + var + lines = newSeqOfCap[Slice[int]](arrangement.lines.len()) + spanSlices = newSeqOfCap[Slice[int]](arrangement.spans.len()) + selectionRects = newSeqOfCap[Rect](arrangement.selectionRects.len()) + for line in arrangement.lines: + lines.add line[0] .. line[1] + for span in arrangement.spans: + spanSlices.add span[0] .. span[1] + for rect in arrangement.selectionRects: + selectionRects.add rect + + result = GlyphArrangement( + contentHash: getContentHash(box.wh, uiSpans, hAlign, vAlign), + lines: lines, # arrangement.lines.toSlices(), + spans: spanSlices, # arrangement.spans.toSlices(), + fonts: gfonts, + runes: arrangement.runes, + positions: arrangement.positions, + selectionRects: selectionRects, + ) + +proc typeset*( + box: Rect, + uiSpans: openArray[(UiFont, string)], + hAlign = FontHorizontal.Left, + vAlign = FontVertical.Top, + minContent: bool, + wrap: bool, +): GlyphArrangement = + ## does the typesetting using pixie, then converts the typeseet results + ## into Figuro's own internal types + ## Primarily done for thread safety + threadEffects: + AppMainThread + + var + wh = box.scaled().wh + sz = uiSpans.mapIt(it[0].size.float) + minSz = sz.foldl(max(a, b), 0.0) + + var spans: seq[Span] + var pfs: seq[Font] + var gfonts: seq[GlyphFont] + for (uiFont, txt) in uiSpans: + let (_, pf) = uiFont.convertFont() + pfs.add(pf) + spans.add(newSpan(txt, pf)) + assert not pf.typeface.isNil + # There's gotta be a better way. Need to lookup the font formulas or equations or something + #let lhAdj = pf.lineHeight + #let lhAdj = max(pf.lineHeight - pf.size, 0.0) + let lhAdj = (pf.lineHeight - pf.size * pf.lineHeight / pf.defaultLineHeight()) / 2 + gfonts.add GlyphFont( + fontId: uiFont.getId(), lineHeight: pf.lineHeight, descentAdj: lhAdj ) - proc typeset*( - box: Rect, - uiSpans: openArray[(UiFont, string)], - hAlign = FontHorizontal.Left, - vAlign = FontVertical.Top, - minContent: bool, - wrap: bool, - ): GlyphArrangement = - ## does the typesetting using pixie, then converts the typeseet results - ## into Figuro's own internal types - ## Primarily done for thread safety - threadEffects: - AppMainThread - - var - wh = box.scaled().wh - sz = uiSpans.mapIt(it[0].size.float) - minSz = sz.foldl(max(a, b), 0.0) - - var spans: seq[Span] - var pfs: seq[Font] - var gfonts: seq[GlyphFont] - for (uiFont, txt) in uiSpans: - let (_, pf) = uiFont.convertFont() - pfs.add(pf) - spans.add(newSpan(txt, pf)) - assert not pf.typeface.isNil - # There's gotta be a better way. Need to lookup the font formulas or equations or something - #let lhAdj = pf.lineHeight - #let lhAdj = max(pf.lineHeight - pf.size, 0.0) - let lhAdj = (pf.lineHeight - pf.size * pf.lineHeight / - pf.defaultLineHeight()) / 2 - gfonts.add GlyphFont( - fontId: uiFont.getId(), lineHeight: pf.lineHeight, descentAdj: lhAdj - ) - - var ha: HorizontalAlignment - case hAlign - of Left: - ha = LeftAlign - of Center: - ha = CenterAlign - of Right: - ha = RightAlign - - var va: VerticalAlignment - case vAlign - of Top: - va = TopAlign - of Middle: - va = MiddleAlign - of Bottom: - va = BottomAlign - - let arrangement = - pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) - result = convertArrangement(arrangement, box, uiSpans, hAlign, vAlign, gfonts) - - let content = result.calcMinMaxContent() - result.minSize = content.minSize - result.maxSize = content.maxSize - result.bounding = content.bounding - - if minContent: - ## calcaulate min width of content - var wh = wh - wh.y = result.maxSize.y.scaled() - let arr = pixie.typeset( - spans, bounds = wh, hAlign = LeftAlign, vAlign = TopAlign, wrap = wrap - ) - let minResult = convertArrangement(arr, box, uiSpans, hAlign, vAlign, gfonts) - - let minContent = minResult.calcMinMaxContent() - trace "minContent:", + var ha: HorizontalAlignment + case hAlign + of Left: + ha = LeftAlign + of Center: + ha = CenterAlign + of Right: + ha = RightAlign + + var va: VerticalAlignment + case vAlign + of Top: + va = TopAlign + of Middle: + va = MiddleAlign + of Bottom: + va = BottomAlign + + let arrangement = + pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) + result = convertArrangement(arrangement, box, uiSpans, hAlign, vAlign, gfonts) + + let content = result.calcMinMaxContent() + result.minSize = content.minSize + result.maxSize = content.maxSize + result.bounding = content.bounding + + if minContent: + ## calcaulate min width of content + var wh = wh + wh.y = result.maxSize.y.scaled() + let arr = pixie.typeset( + spans, bounds = wh, hAlign = LeftAlign, vAlign = TopAlign, wrap = wrap + ) + let minResult = convertArrangement(arr, box, uiSpans, hAlign, vAlign, gfonts) + + let minContent = minResult.calcMinMaxContent() + trace "minContent:", + boxWh = box.wh, + wh = wh, + minSize = minContent.minSize, + maxSize = minContent.maxSize, + bounding = minContent.bounding, + boundH = result.bounding.h + + if minContent.bounding.h > result.bounding.h: + let wh = vec2(wh.x, minContent.bounding.h.scaled()) + let minAdjusted = + pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) + result = convertArrangement(minAdjusted, box, uiSpans, hAlign, vAlign, gfonts) + let contentAdjusted = result.calcMinMaxContent() + result.minSize = contentAdjusted.minSize + result.maxSize = contentAdjusted.maxSize + result.bounding = contentAdjusted.bounding + trace "minContent:adjusted", boxWh = box.wh, wh = wh, - minSize = minContent.minSize, - maxSize = minContent.maxSize, - bounding = minContent.bounding, - boundH = result.bounding.h - - if minContent.bounding.h > result.bounding.h: - let wh = vec2(wh.x, minContent.bounding.h.scaled()) - let minAdjusted = - pixie.typeset(spans, bounds = wh, hAlign = ha, vAlign = va, wrap = wrap) - result = convertArrangement(minAdjusted, box, uiSpans, hAlign, vAlign, gfonts) - let contentAdjusted = result.calcMinMaxContent() - result.minSize = contentAdjusted.minSize - result.maxSize = contentAdjusted.maxSize - result.bounding = contentAdjusted.bounding - trace "minContent:adjusted", - boxWh = box.wh, - wh = wh, - wrap = wrap, - minSize = result.minSize, - maxSize = result.maxSize, - bounding = result.bounding - - result.minSize.y = result.bounding.h - else: - result.minSize.y = max(result.minSize.y, result.bounding.h) - - let maxLineHeight = max(sz) - result.minSize += vec2(maxLineHeight / 2, 0) - result.maxSize += vec2(maxLineHeight / 2, 0) - result.bounding = result.bounding + rect(0, 0, 0, maxLineHeight / 2) - # debug "getTypesetImpl:post:", boxWh= box.wh, wh= wh, contentHash = getContentHash(box.wh, uiSpans, hAlign, vAlign), - # minSize = result.minSize, maxSize = result.maxSize, bounding = result.bounding - - result.generateGlyphImage() - # echo "font: " - # print arrangement.fonts[0].size - # print arrangement.fonts[0].lineHeight - # echo "arrangement: " - # print result - - proc glyphFontFor(uiFont: UiFont): tuple[id: FontId, font: Font, - glyph: GlyphFont] = - let (fontId, pf) = uiFont.convertFont() - let defaultLineHeight = pf.defaultLineHeight() - let lineHeight = - if pf.lineHeight >= 0: - pf.lineHeight - else: - defaultLineHeight - let lhAdj = - if defaultLineHeight > 0: - (lineHeight - pf.size * lineHeight / defaultLineHeight) / 2 - else: - 0.0'f32 - result = ( - id: fontId, - font: pf, - glyph: GlyphFont(fontId: fontId, lineHeight: lineHeight, - descentAdj: lhAdj), + wrap = wrap, + minSize = result.minSize, + maxSize = result.maxSize, + bounding = result.bounding + + result.minSize.y = result.bounding.h + else: + result.minSize.y = max(result.minSize.y, result.bounding.h) + + let maxLineHeight = max(sz) + result.minSize += vec2(maxLineHeight / 2, 0) + result.maxSize += vec2(maxLineHeight / 2, 0) + result.bounding = result.bounding + rect(0, 0, 0, maxLineHeight / 2) + # debug "getTypesetImpl:post:", boxWh= box.wh, wh= wh, contentHash = getContentHash(box.wh, uiSpans, hAlign, vAlign), + # minSize = result.minSize, maxSize = result.maxSize, bounding = result.bounding + + result.generateGlyphImage() + # echo "font: " + # print arrangement.fonts[0].size + # print arrangement.fonts[0].lineHeight + # echo "arrangement: " + # print result + +proc glyphFontFor(uiFont: UiFont): tuple[id: FontId, font: Font, + glyph: GlyphFont] = + let (fontId, pf) = uiFont.convertFont() + let defaultLineHeight = pf.defaultLineHeight() + let lineHeight = + if pf.lineHeight >= 0: + pf.lineHeight + else: + defaultLineHeight + let lhAdj = + if defaultLineHeight > 0: + (lineHeight - pf.size * lineHeight / defaultLineHeight) / 2 + else: + 0.0'f32 + result = ( + id: fontId, + font: pf, + glyph: GlyphFont(fontId: fontId, lineHeight: lineHeight, descentAdj: lhAdj), + ) + +proc placeGlyphs*( + font: UiFont, + glyphs: openArray[(Rune, Vec2)], + origin: GlyphOrigin = GlyphTopLeft, +): GlyphArrangement = + ## Builds a glyph arrangement using explicit positions for each glyph. + ## `origin` controls whether positions are the glyph's top-left or baseline. + threadEffects: + AppMainThread + + result = GlyphArrangement() + if glyphs.len == 0: + return + + let fontInfo = glyphFontFor(font) + let cachedFont = (font: fontInfo.font, glyph: fontInfo.glyph) + + var + runes = newSeqOfCap[Rune](glyphs.len) + positions = newSeqOfCap[Vec2](glyphs.len) + selectionRects = newSeqOfCap[Rect](glyphs.len) + contentHash = Hash(0) + + for (rune, pos) in glyphs: + + let scaledPos = pos.scaled() + let descent = cachedFont.glyph.lineHeight - cachedFont.glyph.descentAdj + var baselinePos = scaledPos + if origin == GlyphTopLeft: + baselinePos.y = scaledPos.y + descent + + runes.add(rune) + positions.add(baselinePos) + + let drawPos = vec2(baselinePos.x, baselinePos.y - descent) + let advance = cachedFont.font.typeface.getAdvance(rune) * + cachedFont.font.scale + selectionRects.add( + rect(drawPos.x, drawPos.y, advance, cachedFont.glyph.lineHeight) ) - proc placeGlyphs*( - font: UiFont, - glyphs: openArray[(Rune, Vec2)], - origin: GlyphOrigin = GlyphTopLeft, - ): GlyphArrangement = - ## Builds a glyph arrangement using explicit positions for each glyph. - ## `origin` controls whether positions are the glyph's top-left or baseline. - threadEffects: - AppMainThread - - result = GlyphArrangement() - if glyphs.len == 0: - return - - let fontInfo = glyphFontFor(font) - let cachedFont = (font: fontInfo.font, glyph: fontInfo.glyph) - - var - runes = newSeqOfCap[Rune](glyphs.len) - positions = newSeqOfCap[Vec2](glyphs.len) - selectionRects = newSeqOfCap[Rect](glyphs.len) - contentHash = Hash(0) - - for (rune, pos) in glyphs: - - let scaledPos = pos.scaled() - let descent = cachedFont.glyph.lineHeight - cachedFont.glyph.descentAdj - var baselinePos = scaledPos - if origin == GlyphTopLeft: - baselinePos.y = scaledPos.y + descent - - runes.add(rune) - positions.add(baselinePos) - - let drawPos = vec2(baselinePos.x, baselinePos.y - descent) - let advance = cachedFont.font.typeface.getAdvance(rune) * - cachedFont.font.scale - selectionRects.add( - rect(drawPos.x, drawPos.y, advance, cachedFont.glyph.lineHeight) - ) - - contentHash = contentHash !& hash((font.getId(), rune, pos.x, pos.y, origin)) - - result.lines = @[0 .. glyphs.len - 1] - result.spans = @[0 .. glyphs.len - 1] - result.fonts = @[cachedFont.glyph] - result.runes = runes - result.positions = positions - result.selectionRects = selectionRects - result.contentHash = !$contentHash - - var - minX = float32.high - minY = float32.high - maxX = -float32.high - maxY = -float32.high - for rect in selectionRects: - minX = min(minX, rect.x) - minY = min(minY, rect.y) - maxX = max(maxX, rect.x + rect.w) - maxY = max(maxY, rect.y + rect.h) - if selectionRects.len > 0: - let boundingScaled = rect(minX, minY, maxX - minX, maxY - minY) - result.bounding = boundingScaled.descaled() - result.minSize = result.bounding.wh - result.maxSize = result.bounding.wh - - result.generateGlyphImage() - - type Box* = Rect - - proc getTypesetImpl*( - box: Box, - spans: openArray[(UiFont, string)], - hAlign = Left, - vAlign = Top, - minContent = false, - wrap = true, - ): GlyphArrangement = - typeset(box, spans, hAlign, vAlign, minContent, wrap) + contentHash = contentHash !& hash((font.getId(), rune, pos.x, pos.y, origin)) + + result.lines = @[0 .. glyphs.len - 1] + result.spans = @[0 .. glyphs.len - 1] + result.fonts = @[cachedFont.glyph] + result.runes = runes + result.positions = positions + result.selectionRects = selectionRects + result.contentHash = !$contentHash + + var + minX = float32.high + minY = float32.high + maxX = -float32.high + maxY = -float32.high + for rect in selectionRects: + minX = min(minX, rect.x) + minY = min(minY, rect.y) + maxX = max(maxX, rect.x + rect.w) + maxY = max(maxY, rect.y + rect.h) + if selectionRects.len > 0: + let boundingScaled = rect(minX, minY, maxX - minX, maxY - minY) + result.bounding = boundingScaled.descaled() + result.minSize = result.bounding.wh + result.maxSize = result.bounding.wh + + result.generateGlyphImage() diff --git a/src/figdraw/common/fontutils_js.nim b/src/figdraw/common/fontutils_js.nim new file mode 100644 index 0000000..6af8239 --- /dev/null +++ b/src/figdraw/common/fontutils_js.nim @@ -0,0 +1,28 @@ +import fonttypes +import uimaths + +type TypeFaceKinds* = enum + TTF + OTF + SVG + +type Box* = Rect + +proc getTypefaceImpl*(name: string): FontId = + FontId(0) + +proc getTypefaceImpl*(name, data: string, kind: TypeFaceKinds): FontId = + FontId(0) + +proc getLineHeightImpl*(font: UiFont): float32 = + 0.0 + +proc getTypesetImpl*( + box: Box, + spans: openArray[(UiFont, string)], + hAlign = Left, + vAlign = Top, + minContent = false, + wrap = true, +): GlyphArrangement = + GlyphArrangement() diff --git a/src/figdraw/commons.nim b/src/figdraw/commons.nim index 6992bb8..dea635d 100644 --- a/src/figdraw/commons.nim +++ b/src/figdraw/commons.nim @@ -6,11 +6,12 @@ else: import common/rchannels import common/transfer import common/appframes -import common/fontutils when defined(js): import common/imgutils_js as imgutils + import common/fontutils_js as fontutils else: import common/imgutils + import common/fontutils export shared, uimaths, rchannels export transfer, appframes From e7f74a4f8c938106ede9691b8316324a6549dcfd Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:42:51 -0700 Subject: [PATCH 07/10] add webgl --- src/figdraw/common/shared.nim | 1 - src/figdraw/common/transfer.nim | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/figdraw/common/shared.nim b/src/figdraw/common/shared.nim index 3735317..bc4411a 100644 --- a/src/figdraw/common/shared.nim +++ b/src/figdraw/common/shared.nim @@ -2,7 +2,6 @@ import std/[sequtils, tables, hashes] import std/[unicode, strformat] when not defined(js): import std/os -when not defined(js): import pkg/variant export sequtils, strformat, tables, hashes diff --git a/src/figdraw/common/transfer.nim b/src/figdraw/common/transfer.nim index db04551..91b2375 100644 --- a/src/figdraw/common/transfer.nim +++ b/src/figdraw/common/transfer.nim @@ -106,7 +106,7 @@ proc copyInto*[N](uis: N): Renders = result.layers.sort( proc(x, y: auto): int = - cmp(x[0], y[0]) + cmp(x[0], y[0]) ) # echo "nodes:len: ", result.len() # printRenders(result) From 9c2e054188d152cf6bb698331098e3c09108f360 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:44:25 -0700 Subject: [PATCH 08/10] add webgl --- src/figdraw/commons.nim | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/figdraw/commons.nim b/src/figdraw/commons.nim index dea635d..7d89a6b 100644 --- a/src/figdraw/commons.nim +++ b/src/figdraw/commons.nim @@ -1,17 +1,15 @@ import common/shared import common/uimaths -when defined(js): - import common/rchannels_js as rchannels -else: - import common/rchannels import common/transfer import common/appframes when defined(js): + import common/rchannels_js as rchannels import common/imgutils_js as imgutils import common/fontutils_js as fontutils else: import common/imgutils import common/fontutils + import common/rchannels export shared, uimaths, rchannels export transfer, appframes From 5625ed81698f67275c76c9e8178332cb994ca433 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:48:13 -0700 Subject: [PATCH 09/10] add webgl --- config.nims | 2 ++ src/figdraw/figbasics.nim | 6 ++---- src/figdraw/fignodes.nim | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/config.nims b/config.nims index 3015145..1917df5 100644 --- a/config.nims +++ b/config.nims @@ -15,6 +15,8 @@ task test, "run unit test": exec("nim c examples/opengl_windy_text.nim") exec("nim c examples/sdl2_renderlist.nim") exec("nim c examples/sdl2_renderlist_100.nim") + exec("nim js examples/webgl_renderlist.nim") + exec("nim js examples/webgl_renderlist_100.nim") task emscripten, "build emscripten examples": exec("nim c -d:emscripten examples/opengl_windy_renderlist.nim") diff --git a/src/figdraw/figbasics.nim b/src/figdraw/figbasics.nim index a9a88ab..7654035 100644 --- a/src/figdraw/figbasics.nim +++ b/src/figdraw/figbasics.nim @@ -1,7 +1,5 @@ import std/[options, hashes] import chroma -when not defined(js): - import stack_strings import common/uimaths import common/fonttypes @@ -12,8 +10,6 @@ else: export uimaths, fonttypes, imgutils export options, chroma -when not defined(js): - export stack_strings const FigStringCap* {.intdefine.} = 48 @@ -26,6 +22,8 @@ type when defined(js): type FigName* = string else: + import stack_strings + export stack_strings type FigName* = StackString[FigStringCap] type diff --git a/src/figdraw/fignodes.nim b/src/figdraw/fignodes.nim index 2974e6b..b35d89a 100644 --- a/src/figdraw/fignodes.nim +++ b/src/figdraw/fignodes.nim @@ -45,11 +45,10 @@ type proc `$`*(id: FigIdx): string = "FigIdx(" & $(int(id)) & ")" -when defined(js): - proc toFigName*(s: string): FigName = +proc toFigName*(s: string): FigName = + when defined(js): s -else: - proc toFigName*(s: string): FigName = + else: toStackString(s[0 ..< min(s.len(), s.len())], FigStringCap) when not defined(js): From 7181f96385cee7f219059f69e21d7a61e4137cd1 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 27 Jan 2026 02:52:17 -0700 Subject: [PATCH 10/10] add webgl --- src/figdraw/opengl/glcontext.nim | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/figdraw/opengl/glcontext.nim b/src/figdraw/opengl/glcontext.nim index 4c95f4f..3d9cce4 100644 --- a/src/figdraw/opengl/glcontext.nim +++ b/src/figdraw/opengl/glcontext.nim @@ -1,29 +1,26 @@ +## Copied from Fidget backend, copyright from @treeform applies + import buffers, chroma, hashes, glapi, os, shaders, strformat, strutils, tables, textures, times -when not defined(js): + +when defined(js): + type Flippy* = object + type Image* = object +else: import pixie import pixie/simd -else: - type Image* = object - -## Copied from Fidget backend, copyright from @treeform applies - -import ../utils/logging -import ../commons -when not defined(js): import ../common/formatflippy -else: - type Flippy* = object -import ../fignodes -when not defined(js): import ../utils/drawextras import ../utils/drawboxes import ../utils/drawshadows - export drawextras +import ../utils/logging +import ../commons +import ../fignodes + logScope: scope = "opengl"