From de61cf2ff45cb9d6aa39273647ba1e50ce5fd75c Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Mon, 6 May 2024 22:21:17 +0200 Subject: [PATCH] Begin work on ctx 2d bindings --- example/bezier_curve.odin | 16 ++++ example/setup.js | 4 +- example/setup.odin | 59 +++++++------ example/utils.odin | 2 +- wasm/ctx2d/ctx2d.js | 178 ++++++++++++++++++++++++++++++++++++++ wasm/ctx2d/ctx2d.odin | 69 +++++++++++++++ wasm/webgl/interface.js | 79 ++++++++++------- wasm/webgl/webgl.js | 60 +++++-------- wasm/webgl/webgl2.js | 2 +- 9 files changed, 368 insertions(+), 101 deletions(-) create mode 100644 example/bezier_curve.odin create mode 100644 wasm/ctx2d/ctx2d.js create mode 100644 wasm/ctx2d/ctx2d.odin diff --git a/example/bezier_curve.odin b/example/bezier_curve.odin new file mode 100644 index 0000000..915e798 --- /dev/null +++ b/example/bezier_curve.odin @@ -0,0 +1,16 @@ +//+private file +package example + +import gl "../wasm/webgl" + +@private +State_Bezier_Curve :: struct { +} + +@private +setup_bezier_curve :: proc(s: ^State_Bezier_Curve, program: gl.Program) { +} + +@private +frame_bezier_curve :: proc(s: ^State_Bezier_Curve, delta: f32) { +} diff --git a/example/setup.js b/example/setup.js index 3dbdeb0..ea0d95a 100644 --- a/example/setup.js +++ b/example/setup.js @@ -121,8 +121,8 @@ const src_instance = await wasm.fetchInstanciateWasm(WASM_FILENAME, { env: {}, // TODO odin_env: wasm.env.makeOdinEnv(wasm_state), odin_dom: wasm.dom.makeOdinDOM(wasm_state), - webgl : wasm.webgl.makeOdinWebGL(webgl_state, wasm_state), - webgl2 : wasm.webgl.makeOdinWegGL2(webgl_state, wasm_state), + webgl : wasm.webgl.makeOdinWebGL(wasm_state, webgl_state), + webgl2 : wasm.webgl.makeOdinWegGL2(wasm_state, webgl_state), }) wasm.initWasmState(wasm_state, src_instance) diff --git a/example/setup.odin b/example/setup.odin index 423a622..d5d7f4a 100644 --- a/example/setup.odin +++ b/example/setup.odin @@ -29,6 +29,7 @@ Example_Kind :: enum { Spotlight, Candy, Sol_System, + Bezier_Curve, } example: Example_Kind @@ -71,19 +72,21 @@ demos: [Example_Kind]struct { vs_sources = {#load("./sol_system.vert", string)}, fs_sources = {#load("./sol_system.frag", string)}, }, + .Bezier_Curve = {}, } // state is a union because it is being used by only one of the examples demo_state: struct #raw_union { - rectangle: State_Rectangle, - pyramid: State_Pyramid, - boxes: State_Boxes, - camera: State_Camera, - lighting: State_Lighting, - specular: State_Specular, - spotlight: State_Spotlight, - candy: State_Candy, - sol_system: State_Sol_System, + rectangle: State_Rectangle, + pyramid: State_Pyramid, + boxes: State_Boxes, + camera: State_Camera, + lighting: State_Lighting, + specular: State_Specular, + spotlight: State_Spotlight, + candy: State_Candy, + sol_system: State_Sol_System, + bezier_curve: State_Bezier_Curve, } @@ -163,15 +166,16 @@ start :: proc "c" (ctx: ^runtime.Context, example_kind: Example_Kind) -> (ok: bo gl.UseProgram(program) switch example { - case .Rectangle: setup_rectangle (&demo_state.rectangle, program) - case .Pyramid: setup_pyramid (&demo_state.pyramid, program) - case .Boxes: setup_boxes (&demo_state.boxes, program) - case .Camera: setup_camera (&demo_state.camera, program) - case .Lighting: setup_lighting (&demo_state.lighting, program) - case .Specular: setup_specular (&demo_state.specular, program) - case .Spotlight: setup_spotlight (&demo_state.spotlight, program) - case .Candy: setup_candy (&demo_state.candy, program) - case .Sol_System: setup_sol_system(&demo_state.sol_system, program) + case .Rectangle: setup_rectangle (&demo_state.rectangle, program) + case .Pyramid: setup_pyramid (&demo_state.pyramid, program) + case .Boxes: setup_boxes (&demo_state.boxes, program) + case .Camera: setup_camera (&demo_state.camera, program) + case .Lighting: setup_lighting (&demo_state.lighting, program) + case .Specular: setup_specular (&demo_state.specular, program) + case .Spotlight: setup_spotlight (&demo_state.spotlight, program) + case .Candy: setup_candy (&demo_state.candy, program) + case .Sol_System: setup_sol_system (&demo_state.sol_system, program) + case .Bezier_Curve: setup_bezier_curve(&demo_state.bezier_curve, program) } if err := gl.GetError(); err != gl.NO_ERROR { @@ -195,14 +199,15 @@ frame :: proc "c" (ctx: ^runtime.Context, delta: f32) { } switch example { - case .Rectangle: frame_rectangle (&demo_state.rectangle, delta) - case .Pyramid: frame_pyramid (&demo_state.pyramid, delta) - case .Boxes: frame_boxes (&demo_state.boxes, delta) - case .Camera: frame_camera (&demo_state.camera, delta) - case .Lighting: frame_lighting (&demo_state.lighting, delta) - case .Specular: frame_specular (&demo_state.specular, delta) - case .Spotlight: frame_spotlight (&demo_state.spotlight, delta) - case .Candy: frame_candy (&demo_state.candy, delta) - case .Sol_System: frame_sol_system(&demo_state.sol_system, delta) + case .Rectangle: frame_rectangle (&demo_state.rectangle, delta) + case .Pyramid: frame_pyramid (&demo_state.pyramid, delta) + case .Boxes: frame_boxes (&demo_state.boxes, delta) + case .Camera: frame_camera (&demo_state.camera, delta) + case .Lighting: frame_lighting (&demo_state.lighting, delta) + case .Specular: frame_specular (&demo_state.specular, delta) + case .Spotlight: frame_spotlight (&demo_state.spotlight, delta) + case .Candy: frame_candy (&demo_state.candy, delta) + case .Sol_System: frame_sol_system (&demo_state.sol_system, delta) + case .Bezier_Curve: frame_bezier_curve(&demo_state.bezier_curve, delta) } } diff --git a/example/utils.odin b/example/utils.odin index ac2e298..7eafa0c 100644 --- a/example/utils.odin +++ b/example/utils.odin @@ -574,7 +574,7 @@ vec3_rotate :: proc "contextless" (v, axis: vec3, angle: f32) -> vec3 { } vec3_transform :: proc "contextless" (v: vec3, m: mat4) -> vec3 { - w := m[0][3] * v.x + m[1][3] * v.y + m[2][3] * v.z + m[3][3] // assume v[3] is 1 + w := m[0][3] * v.x + m[1][3] * v.y + m[2][3] * v.z + m[3][3] return { (m[0][0] * v.x + m[1][0] * v.y + m[2][0] * v.z + m[3][0]) / w, diff --git a/wasm/ctx2d/ctx2d.js b/wasm/ctx2d/ctx2d.js new file mode 100644 index 0000000..9dfc94d --- /dev/null +++ b/wasm/ctx2d/ctx2d.js @@ -0,0 +1,178 @@ +import * as mem from "../memory.js" + + +/** @typedef {import("../types.js").WasmState} Wasm_State */ + + +export function Ctx2d_State() { + this.ctx = /** @type {CanvasRenderingContext2D} */ (/** @type {*} */(null)) +} + +/** @enum {typeof CanvasFillRuleEnum[keyof typeof CanvasFillRuleEnum]} */ +const CanvasFillRuleEnum = /** @type {const} */({ + nonzero: 0, + evenodd: 1, +}) + +/** @type {Record} */ +const CANVAS_FILL_RULE = { + 0: "nonzero", + 1: "evenodd", +} + + +/** + * @param {Wasm_State} wasm + * @param {Ctx2d_State} s + * @returns Canvas 2d context bindings for Odin. + */ +export function make_odin_ctx2d(wasm, s) { + return { + /** + * Sets the current 2d context by canvas id. + * @param {number} id_ptr + * @param {number} id_len + * @returns {boolean} */ + setCurrentContextById: (id_ptr, id_len) => { + const id = mem.load_string_raw(wasm.memory.buffer, id_ptr, id_len) + const element = document.getElementById(id) + + if (!(element instanceof HTMLCanvasElement)) return false + + const ctx = element.getContext("2d") + if (!ctx) return false + + s.ctx = ctx + return true + }, + // ------------------------------ / + // COMPOSITING / + // ------------------------------ / + /** @returns {number} */ + getGlobalAlpha() { + return s.ctx.globalAlpha + }, + /** + * @param {number} alpha + * @returns {void} */ + setGlobalAlpha(alpha) { + s.ctx.globalAlpha = alpha + }, + /** @returns {number} */ + getGlobalCompositeOperation() { + switch (s.ctx.globalCompositeOperation) { + case "source-over": return 0 + case "source-in": return 1 + case "source-out": return 2 + case "source-atop": return 3 + case "destination-over": return 4 + case "destination-in": return 5 + case "destination-out": return 6 + case "destination-atop": return 7 + case "lighter": return 8 + case "copy": return 9 + case "xor": return 10 + case "multiply": return 11 + case "screen": return 12 + case "overlay": return 13 + case "darken": return 14 + case "lighten": return 15 + case "color-dodge": return 16 + case "color-burn": return 17 + case "hard-light": return 18 + case "soft-light": return 19 + case "difference": return 20 + case "exclusion": return 21 + case "hue": return 22 + case "saturation": return 23 + case "color": return 24 + case "luminosity": return 25 + } + }, + /** + * @param {number} op + * @returns {void} */ + setGlobalCompositeOperation(op) { + switch (op) { + case 0: s.ctx.globalCompositeOperation = "source-over" ;break + case 1: s.ctx.globalCompositeOperation = "source-in" ;break + case 2: s.ctx.globalCompositeOperation = "source-out" ;break + case 3: s.ctx.globalCompositeOperation = "source-atop" ;break + case 4: s.ctx.globalCompositeOperation = "destination-over" ;break + case 5: s.ctx.globalCompositeOperation = "destination-in" ;break + case 6: s.ctx.globalCompositeOperation = "destination-out" ;break + case 7: s.ctx.globalCompositeOperation = "destination-atop" ;break + case 8: s.ctx.globalCompositeOperation = "lighter" ;break + case 9: s.ctx.globalCompositeOperation = "copy" ;break + case 10: s.ctx.globalCompositeOperation = "xor" ;break + case 11: s.ctx.globalCompositeOperation = "multiply" ;break + case 12: s.ctx.globalCompositeOperation = "screen" ;break + case 13: s.ctx.globalCompositeOperation = "overlay" ;break + case 14: s.ctx.globalCompositeOperation = "darken" ;break + case 15: s.ctx.globalCompositeOperation = "lighten" ;break + case 16: s.ctx.globalCompositeOperation = "color-dodge" ;break + case 17: s.ctx.globalCompositeOperation = "color-burn" ;break + case 18: s.ctx.globalCompositeOperation = "hard-light" ;break + case 19: s.ctx.globalCompositeOperation = "soft-light" ;break + case 20: s.ctx.globalCompositeOperation = "difference" ;break + case 21: s.ctx.globalCompositeOperation = "exclusion" ;break + case 22: s.ctx.globalCompositeOperation = "hue" ;break + case 23: s.ctx.globalCompositeOperation = "saturation" ;break + case 24: s.ctx.globalCompositeOperation = "color" ;break + case 25: s.ctx.globalCompositeOperation = "luminosity" ;break + } + }, + // ------------------------------ / + // DRAW PATH / + // ------------------------------ / + /** + * Begins a new path. + * @returns {void} + */ + beginPath() { + s.ctx.beginPath(); + }, + /** + * Clips the current path. + * @param {CanvasFillRuleEnum} fill_rule + * @returns {void} + */ + clip(fill_rule) { + s.ctx.clip(CANVAS_FILL_RULE[fill_rule]); + }, + /** + * Fills the current path. + * @param {CanvasFillRuleEnum} fill_rule + * @returns {void} + */ + fill(fill_rule) { + s.ctx.fill(CANVAS_FILL_RULE[fill_rule]) + }, + /** + * Checks if the given point is inside the current path. + * @param {number} x + * @param {number} y + * @param {CanvasFillRuleEnum} fill_rule + * @returns {boolean} + */ + isPointInPath(x, y, fill_rule) { + return s.ctx.isPointInPath(x, y, CANVAS_FILL_RULE[fill_rule]) + }, + /** + * Checks if the given point is inside the current stroke. + * @param {number} x + * @param {number} y + * @returns {boolean} + */ + isPointInStroke(x, y) { + return s.ctx.isPointInStroke(x, y) + }, + /** + * Strokes the current path. + * @returns {void} + */ + stroke() { + s.ctx.stroke() + }, + } +} \ No newline at end of file diff --git a/wasm/ctx2d/ctx2d.odin b/wasm/ctx2d/ctx2d.odin new file mode 100644 index 0000000..21aed1c --- /dev/null +++ b/wasm/ctx2d/ctx2d.odin @@ -0,0 +1,69 @@ +package ctx2d + +foreign import "ctx2d" + +GlobalCompositeOperation :: enum { + source_over = 0, + source_in = 1, + source_out = 2, + source_atop = 3, + destination_over = 4, + destination_in = 5, + destination_out = 6, + destination_atop = 7, + lighter = 8, + copy = 9, + xor = 10, + multiply = 11, + screen = 12, + overlay = 13, + darken = 14, + lighten = 15, + color_dodge = 16, + color_burn = 17, + hard_light = 18, + soft_light = 19, + difference = 20, + exclusion = 21, + hue = 22, + saturation = 23, + color = 24, + luminosity = 25, +} + +CanvasFillRule :: enum { + nonzero = 0, + evenodd = 1, +} + +@(default_calling_convention="contextless") +foreign ctx2d { + // Sets the current 2d context by canvas id. + setCurrentContextById :: proc (id: string) -> bool --- + + // ------------------------------ / + // COMPOSITING / + // ------------------------------ / + + getGlobalCompositeOperation :: proc () -> GlobalCompositeOperation --- + setGlobalCompositeOperation :: proc (operation: GlobalCompositeOperation) --- + getGlobalAlpha :: proc () -> f32 --- + setGlobalAlpha :: proc (alpha: f32) --- + + // ------------------------------ / + // DRAW PATH / + // ------------------------------ / + + // Begins a new path. + beginPath :: proc () --- + // Clips the current path. + clip :: proc (fill_rule: CanvasFillRule = .nonzero) --- + // Fills the current path. + fill :: proc (fill_rule: CanvasFillRule = .nonzero) --- + // Checks if the given point is inside the current path. + isPointInPath :: proc (x: f32, y: f32, fill_rule: CanvasFillRule = .nonzero) -> bool --- + // Checks if the given point is inside the current stroke. + isPointInStroke :: proc (x: f32, y: f32) -> bool --- + // Strokes the current path. + stroke :: proc () --- +} diff --git a/wasm/webgl/interface.js b/wasm/webgl/interface.js index 822fe90..87a6b1f 100644 --- a/wasm/webgl/interface.js +++ b/wasm/webgl/interface.js @@ -6,25 +6,24 @@ import * as t from "./types.js" /** @returns {t.WebGLState} */ export function makeWebGLState() { return { - element: null, - /* will be set later, most of the time we want to assert that it's not null */ - ctx: /** @type {any} */ (null), - version: 1, - id_counter: 1, - last_error: 0, - buffers: [null], - programs: [null], - framebuffers: [null], - renderbuffers: [null], - textures: [null], - uniforms: [null], - shaders: [null], - vaos: [null], - queries: [null], - samplers: [null], - transform_feedbacks: [null], - syncs: [null], - program_infos: [null], + element: null, + ctx: /** @type {any} */ (null), // will be set later, most of the time we want to assert that it's not null + version: 1, + id_counter: 1, + last_error: 0, + buffers: [null], + programs: [null], + framebuffers: [null], + renderbuffers: [null], + textures: [null], + uniforms: [null], + shaders: [null], + vaos: [null], + queries: [null], + samplers: [null], + transform_feedbacks: [null], + syncs: [null], + program_infos: [null], } } @@ -33,26 +32,44 @@ export const EMPTY_U8_ARRAY = new Uint8Array(0) export const INVALID_VALUE = 0x0501 export const INVALID_OPERATION = 0x0502 +/** @type {WebGLContextAttributes} */ +export const DEFAULT_CONTEXT_ATTRIBUTES = { + alpha: true, + antialias: true, + depth: true, + premultipliedAlpha: true, +} + /** * @param {t.WebGLState} webgl * @param {HTMLElement | null} element - * @param {WebGLContextAttributes | undefined} context_settings - * @returns {boolean} + * @param {WebGLContextAttributes | undefined} attributes + * @returns {boolean} success */ -export function setCurrentContext(webgl, element, context_settings) { +export function setCurrentContext(webgl, element, attributes) { if (!(element instanceof HTMLCanvasElement)) return false if (webgl.element === element) return true + + /** @type {WebGLRenderingContext | WebGL2RenderingContext | null} */ + let ctx + + ctx = element.getContext("webgl2", attributes) + if (ctx) { + webgl.ctx = ctx + webgl.version = 2 + webgl.element = element + return true + } - const ctx = - element.getContext("webgl2", context_settings) || - element.getContext("webgl", context_settings) - if (!ctx) return false - - webgl.ctx = ctx - webgl.element = element - webgl.version = webgl.ctx.getParameter(0x1f02).indexOf("WebGL 2.0") !== -1 ? 2 : 1 + ctx = element.getContext("webgl", attributes) + if (ctx) { + webgl.ctx = ctx + webgl.version = 1 + webgl.element = element + return true + } - return true + return false } /** diff --git a/wasm/webgl/webgl.js b/wasm/webgl/webgl.js index 20ac2bf..77e1a46 100644 --- a/wasm/webgl/webgl.js +++ b/wasm/webgl/webgl.js @@ -1,59 +1,43 @@ import * as mem from "../memory.js" - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import * as t from "./types.js" +import * as t from "./types.js" import { - setCurrentContext, - EMPTY_U8_ARRAY, - recordError, - newId, - populateUniformTable, - getSource, - INVALID_VALUE, - INVALID_OPERATION, + setCurrentContext, recordError, newId, populateUniformTable, getSource, + EMPTY_U8_ARRAY, INVALID_VALUE, INVALID_OPERATION, DEFAULT_CONTEXT_ATTRIBUTES, } from "./interface.js" -/** @typedef{import("../types.js").WasmState}WasmInstance */ +/** @typedef {import("../types.js").WasmState} WasmInstance */ /** - * @param {t.WebGLState} webgl * @param {WasmInstance} wasm - * @returns WebGL bindings for Odin. + * @param {t.WebGLState} webgl + * @returns WebGL bindings for Odin. */ -export function makeOdinWebGL(webgl, wasm) { +export function makeOdinWebGL(wasm, webgl) { return { /** - * @param {number} name_ptr - * @param {number} name_len + * Sets the current WebGL context with the default attributes. + * @param {number} id_ptr + * @param {number} id_len * @returns {boolean} */ - SetCurrentContextById: (name_ptr, name_len) => { - const name = mem.load_string_raw(wasm.memory.buffer, name_ptr, name_len) - const element = document.getElementById(name) + SetCurrentContextById: (id_ptr, id_len) => { + const id = mem.load_string_raw(wasm.memory.buffer, id_ptr, id_len) + const element = document.getElementById(id) - return setCurrentContext(webgl, element, { - alpha: true, - antialias: true, - depth: true, - premultipliedAlpha: true, - }) + return setCurrentContext(webgl, element, DEFAULT_CONTEXT_ATTRIBUTES) }, /** - * @param {number} name_ptr ElementId - * @param {number} name_len + * @param {number} id_ptr ElementId + * @param {number} id_len * @param {number} attrs Bitset * @returns {boolean} */ - CreateCurrentContextById: (name_ptr, name_len, attrs) => { - const name = mem.load_string_raw(wasm.memory.buffer, name_ptr, name_len) - const element = document.getElementById(name) + CreateCurrentContextById: (id_ptr, id_len, attrs) => { + const id = mem.load_string_raw(wasm.memory.buffer, id_ptr, id_len) + const element = document.getElementById(id) - return setCurrentContext( - webgl, - element, - // prettier-ignore - { + return setCurrentContext(webgl, element, { alpha: !(attrs & (1 << 0)), antialias: !(attrs & (1 << 1)), depth: !(attrs & (1 << 2)), @@ -62,13 +46,11 @@ export function makeOdinWebGL(webgl, wasm) { preserveDrawingBuffer: !!(attrs & (1 << 5)), stencil: !!(attrs & (1 << 6)), desynchronized: !!(attrs & (1 << 7)), - }, - ) + }) }, /** @returns {number} */ // prettier-ignore GetCurrentContextAttributes() { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!webgl.ctx) return 0 const attrs = webgl.ctx.getContextAttributes() diff --git a/wasm/webgl/webgl2.js b/wasm/webgl/webgl2.js index cca76fd..11743b2 100644 --- a/wasm/webgl/webgl2.js +++ b/wasm/webgl/webgl2.js @@ -7,8 +7,8 @@ import {EMPTY_U8_ARRAY, INVALID_OPERATION, newId, recordError} from "./interface /** @returns WebGL 2 bindings for Odin */ export function makeOdinWegGL2( - /** @type {t.WebGLState} */ _webgl, /** @type {import("../types.js").WasmState} */ wasm, + /** @type {t.WebGLState} */ _webgl, ) { const webgl = /** @type {t.WebGL2State} */ (_webgl)