├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── byte_type.ts ├── deno.jsonc ├── deno.lock ├── examples ├── bouncy_rects.ts ├── font │ ├── font.ts │ └── jetbrains-mono.ttf ├── hello.ts ├── raw_window_handle.ts ├── resizable_window.ts ├── sprite │ ├── README.md │ ├── client.ts │ ├── demo.png │ ├── main.ts │ ├── sprite.png │ └── util.ts ├── stars.ts ├── texture │ ├── logo.yuv │ └── texture.ts └── utils.ts ├── mod.ts ├── tests ├── basic.ts ├── deno_logo.png ├── mp3_flag.ts ├── sample_0.mp3 ├── sdl2_image_test.ts └── system.ts └── webgpu-examples └── boids ├── boids.ts ├── compute.wgsl └── shader.wgsl /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | id-token: write 5 | contents: read 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | name: Build ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | fail-fast: false 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: canary 25 | 26 | - name: typecheck & formatting 27 | shell: bash 28 | run: | 29 | set -xeuo pipefail 30 | deno cache --unstable mod.ts 31 | deno fmt --check 32 | 33 | - name: publish 34 | run: deno publish 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bmp 2 | *.dll 3 | 4 | # VS code 5 | .vscode/ 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/dino"] 2 | path = examples/dino 3 | url = https://github.com/nightlyistaken/dino-deno 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Divy Srivastava 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Deno SDL2 2 | 3 | deno_sdl2 provides bindings to sdl2, sdl2_ttf and sdl2_image. 4 | 5 | https://user-images.githubusercontent.com/34997667/160436698-9045ba0c-3fc8-45f9-8038-4194e5d3dcc8.mov 6 | 7 | Minimum supported Deno version: 2.0.0-rc.7 8 | 9 | ### get started 10 | 11 | ```typescript 12 | import { EventType, WindowBuilder } from "jsr:@divy/sdl2@0.14"; 13 | 14 | const window = new WindowBuilder("Hello, Deno!", 640, 480).build(); 15 | const canvas = window.canvas(); 16 | 17 | for await (const event of window.events()) { 18 | if (event.type == EventType.Quit) { 19 | break; 20 | } else if (event.type == EventType.Draw) { 21 | // Rainbow effect 22 | const r = Math.sin(Date.now() / 1000) * 127 + 128; 23 | const g = Math.sin(Date.now() / 1000 + 2) * 127 + 128; 24 | const b = Math.sin(Date.now() / 1000 + 4) * 127 + 128; 25 | canvas.setDrawColor(Math.floor(r), Math.floor(g), Math.floor(b), 255); 26 | canvas.clear(); 27 | canvas.present(); 28 | } 29 | } 30 | ``` 31 | 32 | ```shell 33 | deno run --allow-env --allow-ffi https://jsr.io/@divy/sdl2/0.14.0/examples/hello.ts 34 | ``` 35 | 36 | ### installing sdl2 37 | 38 | Follow https://wiki.libsdl.org/SDL2/Installation to install the dynamic library. 39 | 40 | TL;DR 41 | 42 | MacOS (arm64/x64): 43 | 44 | ```shell 45 | brew install sdl2 sdl2_image sdl2_ttf 46 | ``` 47 | 48 | Make sure the libraries is in your system's library search paths, if not 49 | already: 50 | 51 | ```shell 52 | sudo ln -s /opt/homebrew/lib/libSDL2.dylib /usr/local/lib/ 53 | sudo ln -s /opt/homebrew/lib/libSDL2_image.dylib /usr/local/lib/ 54 | sudo ln -s /opt/homebrew/lib/libSDL2_ttf.dylib /usr/local/lib/ 55 | ``` 56 | 57 | Additionally, you can set `DENO_SDL2_PATH` to point to the directory where these 58 | three libraries are located. 59 | 60 | Windows (x64): 61 | 62 | Grab prebuilt libraries from: 63 | 64 | - https://github.com/libsdl-org/SDL/releases/tag/release-2.28.5 65 | - https://github.com/libsdl-org/SDL_image/releases/tag/release-2.8.1 66 | - https://github.com/libsdl-org/SDL_ttf/releases/tag/release-2.0.18 67 | 68 | Take `SDL2.dll`, `SDL2_image.dll` and `SDL2_ttf.dll` from each respectively and 69 | put them into cwd or `C:\Windows\System32\`. 70 | 71 | Linux (x64): 72 | 73 | ```shell 74 | sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev 75 | ``` 76 | 77 | ### security 78 | 79 | you need `--allow-ffi` to use SDL2. `deno_sdl2` needs access to system's SDL2 80 | library. Deno's permission model does not work well with FFI libraries, use at 81 | your own risk. 82 | 83 | ### projects using `deno_sdl2` 84 | 85 | - https://github.com/dhairy-online/dino-deno 86 | - https://github.com/dhairy-online/flappybird 87 | - https://github.com/load1n9/caviar 88 | - ...insert your project here 89 | 90 | ### license 91 | 92 | MIT 93 | -------------------------------------------------------------------------------- /byte_type.ts: -------------------------------------------------------------------------------- 1 | // This is a copy of https://deno.land/x/byte_type@0.1.7/ffi.ts 2 | 3 | export type TypedArray = 4 | | Uint8Array 5 | | Uint8ClampedArray 6 | | Int8Array 7 | | Uint16Array 8 | | Int16Array 9 | | Uint32Array 10 | | Int32Array 11 | | Float32Array 12 | | Float64Array 13 | | BigUint64Array 14 | | BigInt64Array; 15 | 16 | // deno-fmt-ignore 17 | export type TypedArrayConstructor = 18 | T extends Uint8Array ? Uint8ArrayConstructor 19 | : T extends Uint8ClampedArray ? Uint8ClampedArrayConstructor 20 | : T extends Int8Array ? Int8ArrayConstructor 21 | : T extends Uint16Array ? Uint16ArrayConstructor 22 | : T extends Int16Array ? Int16ArrayConstructor 23 | : T extends Uint32Array ? Uint32ArrayConstructor 24 | : T extends Int32Array ? Int32ArrayConstructor 25 | : T extends Float32Array ? Float32ArrayConstructor 26 | : T extends Float64Array ? Float64ArrayConstructor 27 | : T extends BigUint64Array ? BigUint64ArrayConstructor 28 | : T extends BigInt64Array ? BigInt64ArrayConstructor 29 | : never; 30 | 31 | export type InnerFFIType = T extends FFIType ? I : never; 32 | 33 | export interface FFIType { 34 | size?: number; 35 | read(view: Deno.UnsafePointerView, offset?: number): T; 36 | } 37 | 38 | export type SizedFFIType = FFIType & { size: number }; 39 | 40 | export class I8 implements FFIType { 41 | size = 1; 42 | 43 | read(view: Deno.UnsafePointerView, offset?: number): number { 44 | return view.getInt8(offset); 45 | } 46 | } 47 | 48 | export class U8 implements FFIType { 49 | size = 1; 50 | 51 | read(view: Deno.UnsafePointerView, offset?: number): number { 52 | return view.getUint8(offset); 53 | } 54 | } 55 | 56 | export class I16 implements FFIType { 57 | size = 2; 58 | 59 | read(view: Deno.UnsafePointerView, offset?: number): number { 60 | return view.getInt16(offset); 61 | } 62 | } 63 | 64 | export class U16 implements FFIType { 65 | size = 2; 66 | 67 | read(view: Deno.UnsafePointerView, offset?: number): number { 68 | return view.getUint16(offset); 69 | } 70 | } 71 | 72 | export class I32 implements FFIType { 73 | size = 4; 74 | 75 | read(view: Deno.UnsafePointerView, offset?: number): number { 76 | return view.getInt32(offset); 77 | } 78 | } 79 | 80 | export class U32 implements FFIType { 81 | size = 4; 82 | 83 | read(view: Deno.UnsafePointerView, offset?: number): number { 84 | return view.getUint32(offset); 85 | } 86 | } 87 | 88 | export class I64 implements FFIType { 89 | size = 8; 90 | 91 | read(view: Deno.UnsafePointerView, offset?: number): bigint | number { 92 | return view.getBigInt64(offset); 93 | } 94 | } 95 | 96 | export class U64 implements FFIType { 97 | size = 8; 98 | 99 | read(view: Deno.UnsafePointerView, offset?: number): bigint | number { 100 | return view.getBigUint64(offset); 101 | } 102 | } 103 | 104 | export class F32 implements FFIType { 105 | size = 4; 106 | 107 | read(view: Deno.UnsafePointerView, offset?: number): number { 108 | return view.getFloat32(offset); 109 | } 110 | } 111 | 112 | export class F64 implements FFIType { 113 | size = 8; 114 | 115 | read(view: Deno.UnsafePointerView, offset?: number): number { 116 | return view.getFloat64(offset); 117 | } 118 | } 119 | 120 | export class Bool implements FFIType { 121 | size = 1; 122 | 123 | read(view: Deno.UnsafePointerView, offset?: number): boolean { 124 | return view.getInt8(offset) === 1; 125 | } 126 | } 127 | 128 | export class Struct< 129 | T extends Record>, 130 | V extends Record = { [K in keyof T]: InnerFFIType }, 131 | > implements FFIType { 132 | size: number; 133 | types: T; 134 | 135 | constructor(types: T) { 136 | this.types = types; 137 | this.size = 0; 138 | 139 | for (const type of Object.values(this.types)) { 140 | this.size += type.size; 141 | } 142 | } 143 | 144 | read(view: Deno.UnsafePointerView, offset = 0): V { 145 | const object: Record = {}; 146 | 147 | for (const [key, type] of Object.entries(this.types)) { 148 | object[key] = type.read(view, offset); 149 | offset += type.size; 150 | } 151 | 152 | return object as V; 153 | } 154 | 155 | get( 156 | view: Deno.UnsafePointerView, 157 | offset = 0, 158 | key: K, 159 | ): InnerFFIType | undefined { 160 | for (const [entry, type] of Object.entries(this.types)) { 161 | const value = type.read(view, offset); 162 | offset += type.size; 163 | 164 | if (entry === key) { 165 | return value as InnerFFIType; 166 | } 167 | } 168 | } 169 | } 170 | 171 | export class FixedArray, V> implements FFIType { 172 | size: number; 173 | type: T; 174 | 175 | constructor(type: T, length: number) { 176 | this.type = type; 177 | this.size = length * type.size; 178 | } 179 | 180 | read(view: Deno.UnsafePointerView, offset = 0): V[] { 181 | const array = []; 182 | 183 | for (let i = offset; i < this.size + offset; i += this.type.size) { 184 | array.push(this.type.read(view, i)); 185 | } 186 | 187 | return array; 188 | } 189 | 190 | get(view: Deno.UnsafePointerView, offset = 0, index: number): V { 191 | return this.type.read(view, offset + index * this.type.size); 192 | } 193 | } 194 | 195 | export class Tuple< 196 | T extends [...SizedFFIType[]], 197 | V extends [...unknown[]] = { [I in keyof T]: InnerFFIType }, 198 | > implements SizedFFIType { 199 | size: number; 200 | types: T; 201 | 202 | constructor(types: T) { 203 | this.types = types; 204 | this.size = 0; 205 | 206 | for (const type of types) { 207 | this.size += type.size; 208 | } 209 | } 210 | 211 | read(view: Deno.UnsafePointerView, offset = 0): V { 212 | const tuple = []; 213 | 214 | for (const type of this.types) { 215 | tuple.push(type.read(view, offset)); 216 | offset += type.size; 217 | } 218 | 219 | return tuple as V; 220 | } 221 | 222 | get( 223 | view: Deno.UnsafePointerView, 224 | offset = 0, 225 | index: I, 226 | ): V[I] { 227 | for (let i = 0; i < this.types.length; i++) { 228 | const type = this.types[i]; 229 | const value = type.read(view, offset); 230 | offset += type.size; 231 | 232 | if (index === i) { 233 | return value as V[I]; 234 | } 235 | } 236 | 237 | throw new RangeError("Index is out of range"); 238 | } 239 | } 240 | 241 | export class FixedString implements FFIType { 242 | size: number; 243 | type: SizedFFIType; 244 | 245 | constructor(length: number, type: SizedFFIType = u8) { 246 | this.size = length * type.size; 247 | this.type = type; 248 | } 249 | 250 | read(view: Deno.UnsafePointerView, offset = 0): string { 251 | const array = []; 252 | 253 | for (let i = offset; i < this.size + offset; i += this.type.size) { 254 | array.push(this.type.read(view, i)); 255 | } 256 | 257 | return String.fromCharCode(...array); 258 | } 259 | } 260 | 261 | export class CString implements FFIType { 262 | read(view: Deno.UnsafePointerView, offset?: number): string { 263 | return view.getCString(offset); 264 | } 265 | } 266 | 267 | export class BitFlags8< 268 | T extends Record, 269 | V extends Record = { [K in keyof T]: boolean }, 270 | > implements FFIType { 271 | size = 1; 272 | flags: T; 273 | 274 | constructor(flags: T) { 275 | this.flags = flags; 276 | } 277 | 278 | read(view: Deno.UnsafePointerView, offset?: number): V { 279 | const flags = view.getUint8(offset); 280 | const ret: Record = {}; 281 | 282 | for (const [key, flag] of Object.entries(this.flags)) { 283 | ret[key] = (flags & flag) === flag; 284 | } 285 | 286 | return ret as V; 287 | } 288 | } 289 | 290 | export class BitFlags16< 291 | T extends Record, 292 | V extends Record = { [K in keyof T]: boolean }, 293 | > implements FFIType { 294 | size = 2; 295 | flags: T; 296 | 297 | constructor(flags: T) { 298 | this.flags = flags; 299 | } 300 | 301 | read(view: Deno.UnsafePointerView, offset?: number): V { 302 | const flags = view.getUint16(offset); 303 | const ret: Record = {}; 304 | 305 | for (const [key, flag] of Object.entries(this.flags)) { 306 | ret[key] = (flags & flag) === flag; 307 | } 308 | 309 | return ret as V; 310 | } 311 | } 312 | 313 | export class BitFlags32< 314 | T extends Record, 315 | V extends Record = { [K in keyof T]: boolean }, 316 | > implements FFIType { 317 | size = 4; 318 | flags: T; 319 | 320 | constructor(flags: T) { 321 | this.flags = flags; 322 | } 323 | 324 | read(view: Deno.UnsafePointerView, offset?: number): V { 325 | const flags = view.getUint32(offset); 326 | const ret: Record = {}; 327 | 328 | for (const [key, flag] of Object.entries(this.flags)) { 329 | ret[key] = (flags & flag) === flag; 330 | } 331 | 332 | return ret as V; 333 | } 334 | } 335 | 336 | export class Expect< 337 | V, 338 | T extends FFIType, 339 | > implements FFIType { 340 | size; 341 | type: T; 342 | expected: V; 343 | 344 | constructor(type: T, expected: V) { 345 | this.size = type.size; 346 | this.type = type; 347 | this.expected = expected; 348 | } 349 | 350 | is( 351 | view: Deno.UnsafePointerView, 352 | offset?: number, 353 | value = this.expected, 354 | ): boolean { 355 | return this.type.read(view, offset) === value; 356 | } 357 | 358 | read(view: Deno.UnsafePointerView, offset?: number): V { 359 | const value = this.type.read(view, offset); 360 | 361 | if (value !== this.expected) { 362 | throw new TypeError(`Expected ${this.expected} found ${value}`); 363 | } 364 | 365 | return value; 366 | } 367 | } 368 | 369 | export class TypedArrayFFIType implements FFIType { 370 | size: number; 371 | length: number; 372 | type: TypedArrayConstructor; 373 | 374 | constructor(type: TypedArrayConstructor, length: number) { 375 | this.size = length * type.BYTES_PER_ELEMENT; 376 | this.length = length; 377 | this.type = type; 378 | } 379 | 380 | read(view: Deno.UnsafePointerView, offset?: number): T { 381 | const array = new this.type(this.length); 382 | view.copyInto(array, offset); 383 | return array as T; 384 | } 385 | } 386 | 387 | export class Uint8ArrayFFIType extends TypedArrayFFIType { 388 | constructor(length: number) { 389 | super(Uint8Array, length); 390 | } 391 | } 392 | 393 | export class Uint8ClampedArrayFFIType 394 | extends TypedArrayFFIType { 395 | constructor(length: number) { 396 | super(Uint8ClampedArray, length); 397 | } 398 | } 399 | 400 | export class Int8ArrayFFIType extends TypedArrayFFIType { 401 | constructor(length: number) { 402 | super(Int8Array, length); 403 | } 404 | } 405 | 406 | export class Uint16ArrayFFIType extends TypedArrayFFIType { 407 | constructor(length: number) { 408 | super(Uint16Array, length); 409 | } 410 | } 411 | 412 | export class Int16ArrayFFIType extends TypedArrayFFIType { 413 | constructor(length: number) { 414 | super(Int16Array, length); 415 | } 416 | } 417 | 418 | export class Uint32ArrayFFIType extends TypedArrayFFIType { 419 | constructor(length: number) { 420 | super(Uint32Array, length); 421 | } 422 | } 423 | 424 | export class Int32ArrayFFIType extends TypedArrayFFIType { 425 | constructor(length: number) { 426 | super(Int32Array, length); 427 | } 428 | } 429 | 430 | export class Float32ArrayFFIType extends TypedArrayFFIType { 431 | constructor(length: number) { 432 | super(Float32Array, length); 433 | } 434 | } 435 | 436 | export class Float64ArrayFFIType extends TypedArrayFFIType { 437 | constructor(length: number) { 438 | super(Float64Array, length); 439 | } 440 | } 441 | 442 | export class BigUint64ArrayFFIType extends TypedArrayFFIType { 443 | constructor(length: number) { 444 | super(BigUint64Array, length); 445 | } 446 | } 447 | 448 | export class BigInt64ArrayFFIType extends TypedArrayFFIType { 449 | constructor(length: number) { 450 | super(BigInt64Array, length); 451 | } 452 | } 453 | 454 | export const i8 = new I8(); 455 | export const u8 = new U8(); 456 | export const i16 = new I16(); 457 | export const u16 = new U16(); 458 | export const i32 = new I32(); 459 | export const u32 = new U32(); 460 | export const i64 = new I64(); 461 | export const u64 = new U64(); 462 | export const f32 = new F32(); 463 | export const f64 = new F64(); 464 | export const bool = new Bool(); 465 | export const cstring = new CString(); 466 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@divy/sdl2", 3 | "version": "0.14.0", 4 | "exports": "./mod.ts", 5 | "lint": { 6 | "rules": { 7 | "exclude": [ 8 | // Dumbest rule IMO. I prefer explicitness. 9 | "no-inferrable-types" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "redirects": { 4 | "https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.178.0/testing/asserts.ts" 5 | }, 6 | "remote": { 7 | "https://deno.land/std@0.178.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471", 8 | "https://deno.land/std@0.178.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", 9 | "https://deno.land/std@0.178.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", 10 | "https://deno.land/std@0.178.0/testing/asserts.ts": "984ab0bfb3faeed92ffaa3a6b06536c66811185328c5dd146257c702c41b01ab", 11 | "https://deno.land/x/byte_type@0.1.7/ffi.ts": "5bc603fd9d0b695bcd1bdaeb45e6e34b0925d5cd8da41ceaec290570a55287e0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/bouncy_rects.ts: -------------------------------------------------------------------------------- 1 | import { EventType, WindowBuilder } from "../mod.ts"; 2 | import { FPS } from "./utils.ts"; 3 | 4 | const window = new WindowBuilder("Hello, Deno!", 600, 800).build(); 5 | const canvas = window.canvas(); 6 | 7 | const boxes: { x: number; y: number; dx: number; dy: number }[] = []; 8 | let num_boxes = 5; 9 | 10 | function initBoxes() { 11 | for (let i = 0; i < num_boxes; i++) { 12 | boxes.push({ 13 | x: Math.floor(Math.random() * 600), 14 | y: Math.floor(Math.random() * 800), 15 | dx: 1, 16 | dy: 50, 17 | }); 18 | } 19 | } 20 | 21 | initBoxes(); 22 | function checkCollision( 23 | x1: number, 24 | y1: number, 25 | w1: number, 26 | h1: number, 27 | x2: number, 28 | y2: number, 29 | w2: number, 30 | h2: number, 31 | ) { 32 | return !(x2 > w1 + x1 || x1 > w2 + x2 || y2 > h1 + y1 || y1 > h2 + y2); 33 | } 34 | 35 | // 60 FPS cap 36 | const stepFrame = FPS(); 37 | 38 | function frame() { 39 | canvas.setDrawColor(0, 0, 0, 255); 40 | canvas.clear(); 41 | canvas.setDrawColor(255, 255, 255, 255); 42 | for (let i = 0; i < num_boxes; i++) { 43 | // Gravity 44 | boxes[i].dy += 2; 45 | 46 | boxes[i].x += boxes[i].dx; 47 | boxes[i].y += boxes[i].dy; 48 | 49 | // Bounce 50 | if (boxes[i].y + 20 > 800) { 51 | boxes[i].y = 800 - 20; 52 | boxes[i].dy = -Math.abs(boxes[i].dy); 53 | } else if (boxes[i].y - 20 < 0) { 54 | boxes[i].y = 20; 55 | boxes[i].dy = Math.abs(boxes[i].dy); 56 | } 57 | 58 | if (boxes[i].x + 20 > 600) { 59 | boxes[i].x = 600 - 20; 60 | boxes[i].dx = -Math.abs(boxes[i].dx); 61 | } else if (boxes[i].x - 20 < 0) { 62 | boxes[i].x = 20; 63 | boxes[i].dx = Math.abs(boxes[i].dx); 64 | } 65 | 66 | // Collision with other boxes 67 | for (let j = 0; j < num_boxes; j++) { 68 | if ( 69 | checkCollision( 70 | boxes[i].x, 71 | boxes[i].y, 72 | 20, 73 | 20, 74 | boxes[j].x, 75 | boxes[j].y, 76 | 20, 77 | 20, 78 | ) 79 | ) { 80 | const dx = boxes[j].x - boxes[i].x; 81 | const dy = boxes[j].y - boxes[i].y; 82 | let d = Math.floor(Math.sqrt(dx * dx + dy * dy)); 83 | 84 | if (d === 0) { 85 | d = 1; 86 | } 87 | const unitX = Math.floor(dx / d); 88 | const unitY = Math.floor(dy / d); 89 | 90 | const force = -2; 91 | 92 | const forceX = unitX * force; 93 | const forceY = unitY * force; 94 | 95 | boxes[i].dx += forceX; 96 | boxes[i].dy += forceY; 97 | 98 | boxes[j].dx -= forceX; 99 | boxes[j].dy -= forceY; 100 | } 101 | } 102 | 103 | canvas.fillRect(boxes[i].x, boxes[i].y, 20, 20); 104 | 105 | // Dampening 106 | boxes[i].dy -= boxes[i].dy <= 0 ? 0 : 1; 107 | boxes[i].dx -= boxes[i].dx <= 0 ? 0 : 1; 108 | } 109 | 110 | canvas.present(); 111 | stepFrame(); 112 | } 113 | 114 | // Fire up the event loop 115 | for await (const event of window.events()) { 116 | switch (event.type) { 117 | case EventType.Draw: 118 | await frame(); 119 | break; 120 | case EventType.Quit: 121 | Deno.exit(0); 122 | break; 123 | case EventType.MouseButtonDown: 124 | boxes.push({ 125 | x: event.x, 126 | y: event.y, 127 | dx: 1, 128 | dy: 50, 129 | }); 130 | num_boxes += 1; 131 | break; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /examples/font/font.ts: -------------------------------------------------------------------------------- 1 | import { Color, EventType, WindowBuilder } from "../../mod.ts"; 2 | import { FPS } from "../utils.ts"; 3 | 4 | const stepFrame = FPS(); 5 | const window = new WindowBuilder("deno_sdl2 Font", 800, 600).build(); 6 | const canvas = window.canvas(); 7 | 8 | const font = canvas.loadFont("./examples/font/jetbrains-mono.ttf", 128); 9 | const color = new Color(255, 0, 0); 10 | 11 | const surface = font.renderSolid(Deno.args[0] || "Hello there!", color); 12 | 13 | const creator = canvas.textureCreator(); 14 | const texture = creator.createTextureFromSurface(surface); 15 | 16 | function frame() { 17 | canvas.clear(); 18 | canvas.copy(texture); 19 | canvas.present(); 20 | stepFrame(); 21 | } 22 | 23 | for await (const event of window.events()) { 24 | if (event.type == EventType.Quit) Deno.exit(0); 25 | else if (event.type == EventType.Draw) frame(); 26 | } 27 | -------------------------------------------------------------------------------- /examples/font/jetbrains-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/examples/font/jetbrains-mono.ttf -------------------------------------------------------------------------------- /examples/hello.ts: -------------------------------------------------------------------------------- 1 | import { EventType, WindowBuilder } from "../mod.ts"; 2 | import { FPS } from "../examples/utils.ts"; 3 | 4 | const window = new WindowBuilder("Hello, Deno!", 640, 480).build(); 5 | const canvas = window.canvas(); 6 | 7 | const fps = FPS(); 8 | for await (const event of window.events()) { 9 | fps(); 10 | if (event.type == EventType.Quit) { 11 | break; 12 | } else if (event.type == EventType.Draw) { 13 | // Rainbow effect 14 | const r = Math.sin(Date.now() / 1000) * 127 + 128; 15 | const g = Math.sin(Date.now() / 1000 + 2) * 127 + 128; 16 | const b = Math.sin(Date.now() / 1000 + 4) * 127 + 128; 17 | canvas.setDrawColor(Math.floor(r), Math.floor(g), Math.floor(b), 255); 18 | canvas.clear(); 19 | canvas.present(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/raw_window_handle.ts: -------------------------------------------------------------------------------- 1 | import { EventType, WindowBuilder } from "../mod.ts"; 2 | 3 | await navigator.gpu.requestAdapter(); 4 | 5 | const window = new WindowBuilder("Raw window handle", 640, 480).build(); 6 | const _surface = window.windowSurface(); 7 | 8 | for await (const event of window.events()) { 9 | if (event.type === EventType.Quit) break; 10 | } 11 | -------------------------------------------------------------------------------- /examples/resizable_window.ts: -------------------------------------------------------------------------------- 1 | import { EventType, WindowBuilder } from "../mod.ts"; 2 | import { FPS } from "../examples/utils.ts"; 3 | 4 | const window = new WindowBuilder("Hello, Deno!", 640, 480).resizable().build(); 5 | const canvas = window.canvas(); 6 | 7 | const fps = FPS(); 8 | for await (const event of window.events()) { 9 | fps(); 10 | if (event.type == EventType.Quit) { 11 | break; 12 | } else if (event.type == EventType.Draw) { 13 | canvas.setDrawColor(0, 0, 0, 255); 14 | canvas.clear(); 15 | canvas.present(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/sprite/README.md: -------------------------------------------------------------------------------- 1 | # Sprite Example 2 | 3 | A sample app showing how to use sprites / tilemap. 4 | 5 | # Usage 6 | 7 | > deno run --unstable -A main.ts 8 | 9 | # Author 10 | 11 | [hashrock](https://github.com/hashrock) 12 | 13 | # License 14 | 15 | Source code and images are licensed under MIT. Please feel free to use them for 16 | your app. 17 | -------------------------------------------------------------------------------- /examples/sprite/client.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/examples/sprite/client.ts -------------------------------------------------------------------------------- /examples/sprite/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/examples/sprite/demo.png -------------------------------------------------------------------------------- /examples/sprite/main.ts: -------------------------------------------------------------------------------- 1 | import { EventType, Rect, Surface, WindowBuilder } from "../../mod.ts"; 2 | import { drawMap, Sprite } from "./util.ts"; 3 | 4 | function sleepSync(ms: number) { 5 | const start = Date.now(); 6 | while (true) { 7 | if (Date.now() - start > ms) { 8 | break; 9 | } 10 | } 11 | } 12 | 13 | const canvasSize = { width: 1000, height: 800 }; 14 | const window = new WindowBuilder( 15 | "Hello, Deno!", 16 | canvasSize.width, 17 | canvasSize.height, 18 | ).build(); 19 | const canv = window.canvas(); 20 | 21 | const surface = Surface.fromFile("./examples/sprite/sprite.png"); 22 | 23 | const creator = canv.textureCreator(); 24 | const texture = creator.createTextureFromSurface(surface); 25 | 26 | const map = [ 27 | [8, 8, 9, 8, 11, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8], 28 | [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8], 29 | [8, 10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8], 30 | [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8], 31 | [8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 8, 8], 32 | [8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8], 33 | [10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8], 34 | [8, 8, 11, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8], 35 | [8, 8, 9, 8, 11, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8], 36 | [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8], 37 | [8, 10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8], 38 | [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8], 39 | [8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 8, 8], 40 | [8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8], 41 | [10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8], 42 | [8, 8, 11, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8], 43 | ]; 44 | 45 | const denoTextureFrames = [ 46 | new Rect(0, 0, 16, 16), 47 | new Rect(16, 0, 16, 16), 48 | new Rect(32, 0, 16, 16), 49 | new Rect(48, 0, 16, 16), 50 | ]; 51 | 52 | const shadowTexture = [ 53 | new Rect(0, 3 * 16, 16, 16), 54 | ]; 55 | 56 | function random(min: number, max: number) { 57 | return (Math.random() * (max - min) + min) | 0; 58 | } 59 | 60 | function createShadowInstance() { 61 | const shadow = new Sprite(texture, shadowTexture); 62 | shadow.x = 0; 63 | shadow.y = 0; 64 | shadow.originX = shadow.frames[0].width / 2 + 6; 65 | shadow.originY = shadow.frames[0].height - 16; 66 | shadow.scale = 4; 67 | shadow.vx = 0; 68 | shadow.vy = 0; 69 | return shadow; 70 | } 71 | 72 | function createDenoInstance(id) { 73 | const deno = new Sprite(texture, denoTextureFrames); 74 | deno.x = random(0, canvasSize.width); 75 | deno.y = random(0, canvasSize.height); 76 | deno.originX = deno.frames[0].width / 2; 77 | deno.originY = deno.frames[0].height; 78 | deno.scale = 4; 79 | deno.vx = 0; 80 | deno.vy = 0; 81 | deno.id = id || 0; 82 | return deno; 83 | } 84 | 85 | const denos: Sprite[] = []; 86 | const self = createDenoInstance(); 87 | const shadow = createShadowInstance(); 88 | 89 | let cnt = 0; 90 | const mouse = { x: 0, y: 0 }; 91 | 92 | function frame() { 93 | canv.clear(); 94 | const tiles = drawMap(texture, canv, map, 16); 95 | 96 | for (const deno of [...denos, self]) { 97 | deno.tick(); 98 | shadow.draw(canv); 99 | deno.draw(canv); 100 | 101 | const margin = 48; 102 | deno.wrap({ 103 | x: -margin, 104 | y: -margin, 105 | width: canvasSize.width + margin * 2, 106 | height: canvasSize.height + margin * 2, 107 | }); 108 | 109 | shadow.x = deno.x; 110 | shadow.y = deno.y; 111 | 112 | // make deno jump 113 | deno.z = Math.abs(Math.sin(cnt / 10) * 16) | 0; 114 | 115 | if ((cnt / 20 | 0) % 2 === 0) { 116 | if (deno.vx > 0) { 117 | deno.index = 2; 118 | } else { 119 | deno.index = 0; 120 | } 121 | } else { 122 | if (deno.vx > 0) { 123 | deno.index = 3; 124 | } else { 125 | deno.index = 1; 126 | } 127 | } 128 | 129 | deno.vx = (mouse.x - deno.x) / 100; 130 | deno.vy = (mouse.y - deno.y) / 100; 131 | 132 | // check for collision with tiles on map 133 | for (const tile of tiles) { 134 | if ( 135 | deno.x < tile.x + tile.width && 136 | deno.x + deno.frames[0].width * deno.scale > tile.x && 137 | deno.y < tile.y + tile.height && 138 | deno.y + deno.frames[0].height * deno.scale > tile.y 139 | ) { 140 | deno.x -= deno.vx; 141 | deno.y -= deno.vy; 142 | } 143 | } 144 | } 145 | 146 | cnt++; 147 | canv.present(); 148 | 149 | sleepSync(10); 150 | } 151 | 152 | for await (const event of window.events()) { 153 | switch (event.type) { 154 | case EventType.Draw: 155 | frame(); 156 | break; 157 | case EventType.Quit: 158 | Deno.exit(0); 159 | break; 160 | case EventType.MouseMotion: 161 | mouse.x = event.x; 162 | mouse.y = event.y; 163 | break; 164 | default: 165 | break; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /examples/sprite/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/examples/sprite/sprite.png -------------------------------------------------------------------------------- /examples/sprite/util.ts: -------------------------------------------------------------------------------- 1 | import { type Canvas, Rect, type Texture } from "../../mod.ts"; 2 | 3 | export interface Area { 4 | x: number; 5 | y: number; 6 | width: number; 7 | height: number; 8 | } 9 | 10 | export function drawMap( 11 | texture: Texture, 12 | canvas: Canvas, 13 | map: number[][], 14 | chipSize: number, 15 | ): Rect[] { 16 | const frames: Rect[] = []; 17 | for (let i = 0; i < map.length; i++) { 18 | for (let j = 0; j < map[i].length; j++) { 19 | const chip = map[i][j]; 20 | const src = new Rect( 21 | (chip % 4) * chipSize, 22 | ((chip / 4) | 0) * chipSize, 23 | chipSize, 24 | chipSize, 25 | ); 26 | const dst = new Rect( 27 | i * chipSize * 4, 28 | j * chipSize * 4, 29 | chipSize * 4, 30 | chipSize * 4, 31 | ); 32 | // 9 = cactus 33 | // 8 = normal tile 34 | if (chip === 9) frames.push(dst); 35 | canvas.copy( 36 | texture, 37 | src, 38 | dst, 39 | ); 40 | } 41 | } 42 | return frames; 43 | } 44 | 45 | export class Sprite { 46 | x = 0; 47 | y = 0; 48 | z = 0; 49 | vx = 0; 50 | vy = 0; 51 | originX = 0; 52 | originY = 0; 53 | scale = 1; 54 | texture: Texture; 55 | frames: Rect[]; 56 | index = 0; 57 | 58 | constructor(texture: Texture, frames: Rect[]) { 59 | this.texture = texture; 60 | this.frames = frames; 61 | } 62 | 63 | draw(dest: Canvas) { 64 | const dst = new Rect( 65 | this.x - this.originX, 66 | this.y - this.originY - this.z, 67 | this.frames[this.index].width * this.scale, 68 | this.frames[this.index].height * this.scale, 69 | ); 70 | dest.copy(this.texture, this.frames[this.index], dst); 71 | } 72 | 73 | tick() { 74 | this.x += this.vx; 75 | this.y += this.vy; 76 | } 77 | 78 | wrap(rect: Area) { 79 | this.x = (this.x - rect.x) % rect.width + rect.x; 80 | this.y = (this.y - rect.y) % rect.height + rect.y; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/stars.ts: -------------------------------------------------------------------------------- 1 | import { EventType, WindowBuilder } from "../mod.ts"; 2 | import { FPS } from "./utils.ts"; 3 | 4 | const HEIGHT = 800; 5 | const WIDTH = 600; 6 | 7 | const window = new WindowBuilder("Stars", WIDTH, HEIGHT).build(); 8 | const canvas = window.canvas(); 9 | 10 | const tick = FPS(); 11 | 12 | const star_count = 512; 13 | const depth = 32; 14 | const stars = []; 15 | const cx = WIDTH / 2; 16 | const cy = HEIGHT / 2; 17 | 18 | const random = (u: number, l: number) => Math.random() * (u - l) + l; 19 | 20 | for (let i = 0; i < star_count; i++) { 21 | const star = [random(-25, 25), random(-25, 25), random(1, depth)]; 22 | stars.push(star); 23 | } 24 | 25 | for await (const event of window.events()) { 26 | if (event.type == EventType.Quit) Deno.exit(0); 27 | else if (event.type == EventType.Draw) { 28 | canvas.setDrawColor(0, 0, 0, 255); 29 | canvas.clear(); 30 | 31 | for (let i = 0; i < stars.length; i++) { 32 | const star = stars[i]; 33 | 34 | star[2] -= 0.2; 35 | if (star[2] <= 0) { 36 | star[0] = random(-25, 25); 37 | star[1] = random(-25, 25); 38 | star[2] = depth; 39 | } 40 | 41 | const k = 128 / star[2]; 42 | const x = Math.floor(star[0] * k + cx); 43 | const y = Math.floor(star[1] * k + cy); 44 | 45 | if ((0 <= x && x < WIDTH) && (0 <= y && y < HEIGHT)) { 46 | const size = Math.floor((1 - star[2] / depth) * 3); 47 | const shade = Math.floor((1 - star[2] / depth) * 255); 48 | 49 | canvas.setDrawColor(shade, shade, shade, 255); 50 | canvas.fillRect(x, y, size, size); 51 | } 52 | } 53 | canvas.present(); 54 | tick(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/texture/logo.yuv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/examples/texture/logo.yuv -------------------------------------------------------------------------------- /examples/texture/texture.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventType, 3 | PixelFormat, 4 | TextureAccess, 5 | WindowBuilder, 6 | } from "../../mod.ts"; 7 | 8 | const window = new WindowBuilder("Hello, Deno!", 1024, 1024).build(); 9 | const canvas = window.canvas(); 10 | 11 | const textureCreator = canvas.textureCreator(); 12 | 13 | const texture = textureCreator.createTexture( 14 | PixelFormat.Unknown, 15 | TextureAccess.Streaming, 16 | 1024, 17 | 1024, 18 | ); 19 | const buf = Deno.readFileSync("examples/texture/logo.yuv"); 20 | texture.update(buf, 1024 * 4); 21 | canvas.copy(texture); 22 | canvas.present(); 23 | 24 | event_loop: 25 | for await (const event of window.events()) { 26 | switch (event.type) { 27 | case EventType.Quit: 28 | case EventType.KeyDown: 29 | break event_loop; 30 | default: 31 | break; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/utils.ts: -------------------------------------------------------------------------------- 1 | export const FPS = () => { 2 | let start = performance.now(); 3 | let frames = 0; 4 | return () => { 5 | frames++; 6 | // setTimeout is blocked by the event loop. 7 | if ((performance.now() - start) >= 1000) { 8 | start = performance.now(); 9 | console.log(`FPS: ${frames}`); 10 | frames = 0; 11 | } 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-unused-vars 2 | import { 3 | cstring, 4 | i32, 5 | type SizedFFIType, 6 | Struct, 7 | u16, 8 | u32, 9 | u64, 10 | u8, 11 | } from "./byte_type.ts"; 12 | 13 | let DENO_SDL2_PATH: string | undefined; 14 | try { 15 | DENO_SDL2_PATH = Deno.env.get("DENO_SDL2_PATH"); 16 | } catch (_) { 17 | // ignore, this can only fail if permission is not given 18 | } 19 | 20 | function isMacos() { 21 | return Deno.build.os === "darwin"; 22 | } 23 | 24 | function isLinux() { 25 | return Deno.build.os === "linux"; 26 | } 27 | 28 | function isWindows() { 29 | return Deno.build.os === "windows"; 30 | } 31 | 32 | const OS_PREFIX = Deno.build.os === "windows" ? "" : "lib"; 33 | const OS_SUFFIX = Deno.build.os === "windows" 34 | ? ".dll" 35 | : Deno.build.os === "darwin" 36 | ? ".dylib" 37 | : ".so"; 38 | 39 | function getLibraryPath(lib: string): string { 40 | lib = `${OS_PREFIX}${lib}${OS_SUFFIX}`; 41 | if (DENO_SDL2_PATH) { 42 | return `${DENO_SDL2_PATH}/${lib}`; 43 | } else { 44 | return lib; 45 | } 46 | } 47 | 48 | const sdl2 = Deno.dlopen(getLibraryPath("SDL2"), { 49 | "SDL_Init": { 50 | "parameters": ["u32"], 51 | "result": "i32", 52 | }, 53 | "SDL_InitSubSystem": { 54 | "parameters": ["u32"], 55 | "result": "i32", 56 | }, 57 | "SDL_QuitSubSystem": { 58 | "parameters": ["u32"], 59 | "result": "i32", 60 | }, 61 | "SDL_GetPlatform": { 62 | "parameters": [], 63 | "result": "pointer", 64 | }, 65 | "SDL_GetError": { 66 | "parameters": [], 67 | "result": "pointer", 68 | }, 69 | "SDL_PollEvent": { 70 | "parameters": ["pointer"], 71 | "result": "i32", 72 | }, 73 | "SDL_WaitEvent": { 74 | "parameters": ["pointer"], 75 | "result": "i32", 76 | }, 77 | "SDL_GetCurrentVideoDriver": { 78 | "parameters": [], 79 | "result": "pointer", 80 | }, 81 | "SDL_CreateWindow": { 82 | "parameters": [ 83 | "buffer", 84 | "i32", 85 | "i32", 86 | "i32", 87 | "i32", 88 | "u32", 89 | ], 90 | "result": "pointer", 91 | }, 92 | "SDL_DestroyWindow": { 93 | "parameters": ["pointer"], 94 | "result": "i32", 95 | }, 96 | "SDL_GetWindowSize": { 97 | "parameters": ["pointer", "pointer", "pointer"], 98 | "result": "i32", 99 | }, 100 | "SDL_GetWindowPosition": { 101 | "parameters": ["pointer", "pointer", "pointer"], 102 | "result": "i32", 103 | }, 104 | "SDL_GetWindowFlags": { 105 | "parameters": ["pointer"], 106 | "result": "u32", 107 | }, 108 | "SDL_SetWindowTitle": { 109 | "parameters": ["pointer", "pointer"], 110 | "result": "i32", 111 | }, 112 | "SDL_SetWindowIcon": { 113 | "parameters": ["pointer", "pointer"], 114 | "result": "i32", 115 | }, 116 | "SDL_SetWindowPosition": { 117 | "parameters": ["pointer", "i32", "i32"], 118 | "result": "i32", 119 | }, 120 | "SDL_SetWindowSize": { 121 | "parameters": ["pointer", "i32", "i32"], 122 | "result": "i32", 123 | }, 124 | "SDL_SetWindowFullscreen": { 125 | "parameters": ["pointer", "u32"], 126 | "result": "i32", 127 | }, 128 | "SDL_SetWindowMinimumSize": { 129 | "parameters": ["pointer", "i32", "i32"], 130 | "result": "i32", 131 | }, 132 | "SDL_SetWindowMaximumSize": { 133 | "parameters": ["pointer", "i32", "i32"], 134 | "result": "i32", 135 | }, 136 | "SDL_SetWindowBordered": { 137 | "parameters": ["pointer", "i32"], 138 | "result": "i32", 139 | }, 140 | "SDL_SetWindowResizable": { 141 | "parameters": ["pointer", "i32"], 142 | "result": "i32", 143 | }, 144 | "SDL_SetWindowInputFocus": { 145 | "parameters": ["pointer"], 146 | "result": "i32", 147 | }, 148 | "SDL_SetWindowGrab": { 149 | "parameters": ["pointer", "i32"], 150 | "result": "i32", 151 | }, 152 | "SDL_CreateRenderer": { 153 | "parameters": ["pointer", "i32", "u32"], 154 | "result": "pointer", 155 | }, 156 | "SDL_SetRenderDrawColor": { 157 | "parameters": ["pointer", "u8", "u8", "u8", "u8"], 158 | "result": "i32", 159 | }, 160 | "SDL_RenderClear": { 161 | "parameters": ["pointer"], 162 | "result": "i32", 163 | }, 164 | "SDL_SetRenderDrawBlendMode": { 165 | "parameters": ["pointer", "u32"], 166 | "result": "i32", 167 | }, 168 | "SDL_RenderPresent": { 169 | "parameters": ["pointer"], 170 | "result": "i32", 171 | }, 172 | "SDL_RenderDrawPoint": { 173 | "parameters": ["pointer", "i32", "i32"], 174 | "result": "i32", 175 | }, 176 | "SDL_RenderDrawPoints": { 177 | "parameters": ["pointer", "pointer", "i32"], 178 | "result": "i32", 179 | }, 180 | "SDL_RenderDrawLine": { 181 | "parameters": ["pointer", "i32", "i32", "i32", "i32"], 182 | "result": "i32", 183 | }, 184 | "SDL_RenderDrawLines": { 185 | "parameters": ["pointer", "pointer", "i32"], 186 | "result": "i32", 187 | }, 188 | "SDL_RenderDrawRect": { 189 | "parameters": ["pointer", "pointer"], 190 | "result": "i32", 191 | }, 192 | "SDL_RenderDrawRects": { 193 | "parameters": ["pointer", "pointer", "i32"], 194 | "result": "i32", 195 | }, 196 | "SDL_RenderFillRect": { 197 | "parameters": ["pointer", "pointer"], 198 | "result": "i32", 199 | }, 200 | "SDL_RenderFillRects": { 201 | "parameters": ["pointer", "pointer", "i32"], 202 | "result": "i32", 203 | }, 204 | "SDL_RenderCopy": { 205 | "parameters": ["pointer", "pointer", "pointer", "pointer"], 206 | "result": "i32", 207 | }, 208 | "SDL_RenderCopyEx": { 209 | "parameters": [ 210 | "pointer", 211 | "pointer", 212 | "pointer", 213 | "pointer", 214 | "f32", 215 | "pointer", 216 | "u32", 217 | ], 218 | "result": "i32", 219 | }, 220 | "SDL_RenderReadPixels": { 221 | "parameters": ["pointer", "pointer", "u32", "pointer", "i32"], 222 | "result": "i32", 223 | }, 224 | "SDL_CreateTexture": { 225 | "parameters": ["pointer", "u32", "i32", "i32", "i32"], 226 | "result": "pointer", 227 | }, 228 | "SDL_DestroyTexture": { 229 | "parameters": ["pointer"], 230 | "result": "i32", 231 | }, 232 | "SDL_QueryTexture": { 233 | "parameters": [ 234 | "pointer", 235 | "buffer", 236 | "buffer", 237 | "buffer", 238 | "buffer", 239 | ], 240 | "result": "i32", 241 | }, 242 | "SDL_SetTextureColorMod": { 243 | "parameters": ["pointer", "u8", "u8", "u8"], 244 | "result": "i32", 245 | }, 246 | "SDL_SetTextureAlphaMod": { 247 | "parameters": ["pointer", "u8"], 248 | "result": "i32", 249 | }, 250 | "SDL_UpdateTexture": { 251 | "parameters": ["pointer", "buffer", "buffer", "i32"], 252 | "result": "i32", 253 | }, 254 | "SDL_LoadBMP_RW": { 255 | "parameters": ["buffer"], 256 | "result": "pointer", 257 | }, 258 | "SDL_CreateTextureFromSurface": { 259 | "parameters": ["pointer", "pointer"], 260 | "result": "pointer", 261 | }, 262 | "SDL_GetWindowWMInfo": { 263 | "parameters": ["pointer", "pointer"], 264 | "result": "i32", 265 | }, 266 | "SDL_GetVersion": { 267 | "parameters": ["pointer"], 268 | "result": "i32", 269 | }, 270 | "SDL_Metal_CreateView": { 271 | "parameters": ["pointer"], 272 | "result": "pointer", 273 | }, 274 | "SDL_RaiseWindow": { 275 | "parameters": ["pointer"], 276 | "result": "i32", 277 | }, 278 | "SDL_GetKeyName": { 279 | "parameters": ["i32"], 280 | "result": "pointer", 281 | }, 282 | "SDL_StartTextInput": { 283 | "parameters": [], 284 | "result": "i32", 285 | }, 286 | "SDL_StopTextInput": { 287 | "parameters": [], 288 | "result": "i32", 289 | }, 290 | "SDL_RWFromMem": { 291 | "parameters": ["buffer", "i32"], 292 | "result": "pointer", 293 | }, 294 | }); 295 | 296 | const SDL2_Image_symbols = { 297 | "IMG_Init": { 298 | "parameters": ["u32"], 299 | "result": "u32", 300 | }, 301 | "IMG_Load": { 302 | "parameters": ["buffer"], 303 | "result": "pointer", 304 | }, 305 | "IMG_Load_RW": { 306 | "parameters": ["pointer"], 307 | "result": "pointer", 308 | }, 309 | } as const; 310 | 311 | const SDL2_TTF_symbols = { 312 | "TTF_Init": { 313 | "parameters": [], 314 | "result": "u32", 315 | }, 316 | "TTF_OpenFont": { 317 | "parameters": ["buffer", "i32"], 318 | "result": "pointer", 319 | }, 320 | "TTF_OpenFontRW": { 321 | "parameters": ["pointer", "i32", "i32"], 322 | "result": "pointer", 323 | }, 324 | "TTF_RenderText_Solid": { 325 | "parameters": ["pointer", "buffer", "pointer"], 326 | "result": "pointer", 327 | }, 328 | "TTF_RenderText_Shaded": { 329 | "parameters": ["pointer", "pointer", "pointer", "pointer"], 330 | "result": "pointer", 331 | }, 332 | "TTF_RenderText_Blended": { 333 | "parameters": ["pointer", "buffer", "pointer"], 334 | "result": "pointer", 335 | }, 336 | "TTF_CloseFont": { 337 | "parameters": ["pointer"], 338 | "result": "i32", 339 | }, 340 | "TTF_Quit": { 341 | "parameters": [], 342 | "result": "i32", 343 | }, 344 | } as const; 345 | 346 | let sdl2Image: Deno.DynamicLibrary, 347 | sdl2Font: Deno.DynamicLibrary; 348 | 349 | try { 350 | sdl2Image = Deno.dlopen(getLibraryPath("SDL2_image"), SDL2_Image_symbols); 351 | } catch (_e) { 352 | console.log("SDL2_image not loaded. Some features will not be available."); 353 | } 354 | 355 | try { 356 | sdl2Font = Deno.dlopen(getLibraryPath("SDL2_ttf"), SDL2_TTF_symbols); 357 | } catch (_e) { 358 | console.log("SDL2_ttf not loaded. Some features will not be available."); 359 | } 360 | 361 | let context_alive = false; 362 | function init() { 363 | if (context_alive) { 364 | return; 365 | } 366 | context_alive = true; 367 | const result = sdl2.symbols.SDL_Init(0); 368 | if (result != 0) { 369 | const errPtr = sdl2.symbols.SDL_GetError(); 370 | const view = new Deno.UnsafePointerView(errPtr!); 371 | throw new Error(`SDL_Init failed: ${view.getCString()}`); 372 | } 373 | 374 | const platform = sdl2.symbols.SDL_GetPlatform(); 375 | const view = new Deno.UnsafePointerView(platform!); 376 | console.log(`SDL2 initialized on ${view.getCString()}`); 377 | // Initialize subsystems 378 | // SDL_INIT_EVENTS 379 | { 380 | const result = sdl2.symbols.SDL_InitSubSystem(0x00000001); 381 | if (result != 0) { 382 | const errPtr = sdl2.symbols.SDL_GetError(); 383 | const view = new Deno.UnsafePointerView(errPtr!); 384 | throw new Error(`SDL_InitSubSystem failed: ${view.getCString()}`); 385 | } 386 | } 387 | // SDL_INIT_VIDEO 388 | { 389 | const result = sdl2.symbols.SDL_InitSubSystem(0x00000010); 390 | if (result != 0) { 391 | const errPtr = sdl2.symbols.SDL_GetError(); 392 | const view = new Deno.UnsafePointerView(errPtr!); 393 | throw new Error(`SDL_InitSubSystem failed: ${view.getCString()}`); 394 | } 395 | } 396 | // SDL_INIT_IMAGE 397 | { 398 | const result = sdl2.symbols.SDL_InitSubSystem(0x00000004); 399 | if (result != 0) { 400 | const errPtr = sdl2.symbols.SDL_GetError(); 401 | const view = new Deno.UnsafePointerView(errPtr!); 402 | throw new Error(`SDL_InitSubSystem failed: ${view.getCString()}`); 403 | } 404 | } 405 | // IMG_Init 406 | { 407 | // TIF = 4, WEBP = 8 408 | sdl2Image?.symbols.IMG_Init(1 | 2); // png and jpg 409 | } 410 | // SDL_INIT_TTF 411 | { 412 | const result = sdl2.symbols.SDL_InitSubSystem(0x00000100); 413 | if (result != 0) { 414 | const errPtr = sdl2.symbols.SDL_GetError(); 415 | const view = new Deno.UnsafePointerView(errPtr!); 416 | throw new Error(`SDL_InitSubSystem failed: ${view.getCString()}`); 417 | } 418 | } 419 | // TTF_Init 420 | { 421 | sdl2Font?.symbols.TTF_Init(); 422 | } 423 | } 424 | 425 | init(); 426 | 427 | /** 428 | * An enum that contains structures for the different event types. 429 | */ 430 | export enum EventType { 431 | First = 0, 432 | Quit = 0x100, 433 | AppTerminating = 0x101, 434 | AppLowMemory = 0x102, 435 | AppWillEnterBackground = 0x103, 436 | AppDidEnterBackground = 0x104, 437 | AppWillEnterForeground = 0x105, 438 | AppDidEnterForeground = 0x106, 439 | WindowEvent = 0x200, 440 | KeyDown = 0x300, 441 | KeyUp = 0x301, 442 | TextEditing = 0x302, 443 | TextInput = 0x303, 444 | MouseMotion = 0x400, 445 | MouseButtonDown = 0x401, 446 | MouseButtonUp = 0x402, 447 | MouseWheel = 0x403, 448 | // JoyAxisMotion = 0x600, 449 | // JoyBallMotion = 0x601, 450 | // JoyHatMotion = 0x602, 451 | // JoyButtonDown = 0x603, 452 | // JoyButtonUp = 0x604, 453 | // JoyDeviceAdded = 0x605, 454 | // JoyDeviceRemoved = 0x606, 455 | // ControllerAxisMotion = 0x650, 456 | // ControllerButtonDown = 0x651, 457 | // ControllerButtonUp = 0x652, 458 | // ControllerDeviceAdded = 0x653, 459 | // ControllerDeviceRemoved = 0x654, 460 | // ControllerDeviceRemapped = 0x655, 461 | // FingerDown = 0x700, 462 | // FingerUp = 0x701, 463 | // FingerMotion = 0x702, 464 | // DollarGesture = 0x800, 465 | // DollarRecord = 0x801, 466 | // MultiGesture = 0x802, 467 | // ClipboardUpdate = 0x900, 468 | // DropFile = 0x1000, 469 | // DropText = 0x1001, 470 | // DropBegin = 0x1002, 471 | // DropComplete = 0x1003, 472 | AudioDeviceAdded = 0x1100, 473 | AudioDeviceRemoved = 0x1101, 474 | // RenderTargetsReset = 0x2000, 475 | // RenderDeviceReset = 0x2001, 476 | User = 0x8000, 477 | Last = 0xFFFF, 478 | Draw, 479 | } 480 | 481 | const _raw = Symbol("raw"); 482 | const enc = new TextEncoder(); 483 | 484 | function asCString(str: string): Uint8Array { 485 | return enc.encode(`${str}\0`); 486 | } 487 | 488 | function throwSDLError(): never { 489 | const error = sdl2.symbols.SDL_GetError(); 490 | const view = Deno.UnsafePointerView.getCString(error!); 491 | throw new Error(`SDL Error: ${view}`); 492 | } 493 | 494 | /** 495 | * SDL2 canvas. 496 | */ 497 | export class Canvas { 498 | constructor( 499 | private window: Deno.PointerValue, 500 | private target: Deno.PointerValue, 501 | ) {} 502 | 503 | serialize(): ArrayBuffer { 504 | return new BigUint64Array([Deno.UnsafePointer.value(this.target)]).buffer; 505 | } 506 | 507 | static deserialize(data: ArrayBuffer): Canvas { 508 | const [ptr] = new BigUint64Array(data); 509 | return new Canvas(null!, Deno.UnsafePointer.create(ptr)); 510 | } 511 | 512 | /** 513 | * Set the color used for drawing operations (Rect, Line and Clear). 514 | * @param r the red value used to draw on the rendering target 515 | * @param g the green value used to draw on the rendering target 516 | * @param b the blue value used to draw on the rendering target 517 | * @param a the alpha value used to draw on the rendering target; usually SDL_ALPHA_OPAQUE (255). 518 | */ 519 | setDrawColor(r: number, g: number, b: number, a: number) { 520 | const ret = sdl2.symbols.SDL_SetRenderDrawColor(this.target, r, g, b, a); 521 | if (ret < 0) { 522 | throwSDLError(); 523 | } 524 | } 525 | 526 | /** 527 | * Clear the current rendering target with the drawing color. 528 | */ 529 | clear() { 530 | const ret = sdl2.symbols.SDL_RenderClear(this.target); 531 | if (ret < 0) { 532 | throwSDLError(); 533 | } 534 | } 535 | /** 536 | * Update the screen with any rendering performed since the previous call. 537 | */ 538 | present() { 539 | sdl2.symbols.SDL_RenderPresent(this.target); 540 | } 541 | 542 | /** 543 | * Draw a point on the current rendering target. 544 | * @param x the x coordinate of the point 545 | * @param y the y coordinate of the point 546 | */ 547 | drawPoint(x: number, y: number) { 548 | const ret = sdl2.symbols.SDL_RenderDrawPoint(this.target, x, y); 549 | if (ret < 0) { 550 | throwSDLError(); 551 | } 552 | } 553 | 554 | /** 555 | * Draw multiple points on the current rendering target. 556 | * @param points an array of Points (x, y) structures that represent the points to draw 557 | */ 558 | drawPoints(points: [number, number][]) { 559 | const intArray = new Int32Array(points.flat()); 560 | const ret = sdl2.symbols.SDL_RenderDrawPoints( 561 | this.target, 562 | Deno.UnsafePointer.of(intArray), 563 | intArray.length, 564 | ); 565 | if (ret < 0) { 566 | throwSDLError(); 567 | } 568 | } 569 | 570 | /** 571 | * Draw a line on the current rendering target. 572 | * @param x1 the x coordinate of the start point 573 | * @param y1 the y coordinate of the start point 574 | * @param x2 the x coordinate of the end point 575 | * @param y2 the y coordinate of the end point 576 | */ 577 | drawLine(x1: number, y1: number, x2: number, y2: number) { 578 | const ret = sdl2.symbols.SDL_RenderDrawLine(this.target, x1, y1, x2, y2); 579 | if (ret < 0) { 580 | throwSDLError(); 581 | } 582 | } 583 | 584 | /** 585 | * Draw a series of connected lines on the current rendering target. 586 | * @param points an array of Points (x, y) structures representing points along the lines 587 | */ 588 | drawLines(points: [number, number][]) { 589 | const intArray = new Int32Array(points.flat()); 590 | const ret = sdl2.symbols.SDL_RenderDrawLines( 591 | this.target, 592 | Deno.UnsafePointer.of(intArray), 593 | intArray.length, 594 | ); 595 | if (ret < 0) { 596 | throwSDLError(); 597 | } 598 | } 599 | 600 | /** 601 | * Draw a rectangle on the current rendering target. 602 | * @param x the x coordinate of the rectangle 603 | * @param y the y coordinate of the rectangle 604 | * @param w the width of the rectangle 605 | * @param h the height of the rectangle 606 | */ 607 | 608 | drawRect(x: number, y: number, w: number, h: number) { 609 | const intArray = new Int32Array([x, y, w, h]); 610 | const ret = sdl2.symbols.SDL_RenderDrawRect( 611 | this.target, 612 | Deno.UnsafePointer.of(intArray), 613 | ); 614 | if (ret < 0) { 615 | throwSDLError(); 616 | } 617 | } 618 | 619 | /** 620 | * Draw some number of rectangles on the current rendering target. 621 | * @param rects an array of Rect (x, y, w, h) structures representing the rectangles to draw 622 | */ 623 | drawRects(rects: [number, number, number, number][]) { 624 | const intArray = new Int32Array(rects.flat()); 625 | const ret = sdl2.symbols.SDL_RenderDrawRects( 626 | this.target, 627 | Deno.UnsafePointer.of(intArray), 628 | intArray.length, 629 | ); 630 | if (ret < 0) { 631 | throwSDLError(); 632 | } 633 | } 634 | 635 | /** 636 | * Fill a rectangle on the current rendering target with the drawing color. 637 | * @param x the x coordinate of the rectangle 638 | * @param y the y coordinate of the rectangle 639 | * @param w the width of the rectangle 640 | * @param h the height of the rectangle 641 | */ 642 | fillRect(x: number, y: number, w: number, h: number) { 643 | const intArray = new Int32Array([x, y, w, h]); 644 | const ret = sdl2.symbols.SDL_RenderFillRect( 645 | this.target, 646 | Deno.UnsafePointer.of(intArray), 647 | ); 648 | if (ret < 0) { 649 | throwSDLError(); 650 | } 651 | } 652 | 653 | /** 654 | * Fill some number of rectangles on the current rendering target with the drawing color. 655 | * @param rects an array of Rect (x, y, w, h) structures representing the rectangles to fill 656 | */ 657 | fillRects(rects: [number, number, number, number][]) { 658 | const intArray = new Int32Array(rects.flat()); 659 | const ret = sdl2.symbols.SDL_RenderFillRects( 660 | this.target, 661 | Deno.UnsafePointer.of(intArray), 662 | intArray.length, 663 | ); 664 | if (ret < 0) { 665 | throwSDLError(); 666 | } 667 | } 668 | 669 | /** 670 | * Copy a portion of the texture to the current rendering target. 671 | * @param texture the source texture 672 | * @param source the source rectangle, or null to copy the entire texture 673 | * @param dest the destination rectangle, or null for the entire rendering target; the texture will be stretched to fill the given rectangle 674 | */ 675 | copy(texture: Texture, source?: Rect, dest?: Rect) { 676 | const ret = sdl2.symbols.SDL_RenderCopy( 677 | this.target, 678 | texture[_raw], 679 | source ? Deno.UnsafePointer.of(source[_raw]) : null, 680 | dest ? Deno.UnsafePointer.of(dest[_raw]) : null, 681 | ); 682 | if (ret < 0) { 683 | throwSDLError(); 684 | } 685 | } 686 | 687 | /** 688 | * TextureCreator is a helper class for creating textures. 689 | * @returns a TextureCreator object for use with creating textures 690 | */ 691 | textureCreator(): TextureCreator { 692 | return new TextureCreator(this.target); 693 | } 694 | 695 | /** 696 | * Create a font from a file, using a specified point size. 697 | * @param path the path to the font file 698 | * @param size point size to use for the newly-opened font 699 | * @returns a Font object for use with rendering text 700 | */ 701 | loadFont(path: string, size: number): Font { 702 | const raw = sdl2Font.symbols.TTF_OpenFont(asCString(path), size); 703 | return new Font(raw); 704 | } 705 | 706 | loadFontRaw(data: Uint8Array, size: number): Font { 707 | const rwops = sdl2.symbols.SDL_RWFromMem(data, data.byteLength); 708 | if (rwops === null) { 709 | throwSDLError(); 710 | } 711 | const raw = sdl2Font.symbols.TTF_OpenFontRW(rwops, 1, size); 712 | if (raw === null) { 713 | throwSDLError(); 714 | } 715 | return new Font(raw); 716 | } 717 | } 718 | 719 | /** 720 | * Font is a helper class for rendering text. 721 | */ 722 | export class Font { 723 | [_raw]: Deno.PointerValue; 724 | constructor(raw: Deno.PointerValue) { 725 | this[_raw] = raw; 726 | } 727 | /** 728 | * Render a solid color version of the text. 729 | * @param text text to render, in Latin1 encoding. 730 | * @param color the foreground color of the text 731 | * @returns a Texture object 732 | */ 733 | renderSolid(text: string, color: Color): Surface { 734 | const raw = sdl2Font.symbols.TTF_RenderText_Solid( 735 | this[_raw], 736 | asCString(text), 737 | color[_raw], 738 | ); 739 | return new Surface(raw); 740 | } 741 | 742 | /** 743 | * Render text at high quality to a new ARGB surface. 744 | * @param text text to render, in Latin1 encoding. 745 | * @param color the foreground color of the text 746 | * @returns a Texture object 747 | */ 748 | renderBlended(text: string, color: Color): Surface { 749 | const raw = sdl2Font.symbols.TTF_RenderText_Blended( 750 | this[_raw], 751 | asCString(text), 752 | color[_raw], 753 | ); 754 | return new Surface(raw); 755 | } 756 | } 757 | 758 | /** 759 | * Color is a helper class for representing colors. 760 | */ 761 | export class Color { 762 | [_raw]: Deno.PointerValue; 763 | constructor(r: number, g: number, b: number, a: number = 0xff) { 764 | const raw = new Uint8Array([r, g, b, a]); 765 | this[_raw] = Deno.UnsafePointer.of(raw); 766 | } 767 | } 768 | /** 769 | * A structure that contains pixel format information. 770 | * @see https://wiki.libsdl.org/SDL2/SDL_PixelFormat 771 | */ 772 | export enum PixelFormat { 773 | Unknown = 0, 774 | Index1LSB = 286261504, 775 | Index1MSB = 287310080, 776 | Index4LSB = 303039488, 777 | Index4MSB = 304088064, 778 | Index8 = 318769153, 779 | RGB332 = 336660481, 780 | XRGB4444 = 353504258, 781 | XBGR4444 = 357698562, 782 | XRGB1555 = 353570562, 783 | XBGR1555 = 357764866, 784 | ARGB4444 = 355602434, 785 | RGBA4444 = 356651010, 786 | ABGR4444 = 359796738, 787 | BGRA4444 = 360845314, 788 | ARGB1555 = 355667970, 789 | RGBA5551 = 356782082, 790 | ABGR1555 = 359862274, 791 | BGRA5551 = 360976386, 792 | RGB565 = 353701890, 793 | BGR565 = 357896194, 794 | RGB24 = 386930691, 795 | BGR24 = 390076419, 796 | XRGB8888 = 370546692, 797 | RGBX8888 = 371595268, 798 | XBGR8888 = 374740996, 799 | BGRX8888 = 375789572, 800 | ARGB8888 = 372645892, 801 | RGBA8888 = 373694468, 802 | ABGR8888 = 376840196, 803 | BGRA8888 = 377888772, 804 | ARGB2101010 = 372711428, 805 | YV12 = 842094169, 806 | IYUV = 1448433993, 807 | YUY2 = 844715353, 808 | UYVY = 1498831189, 809 | YVYU = 1431918169, 810 | } 811 | 812 | /** 813 | * An enumeration of texture access patterns. 814 | * @see https://wiki.libsdl.org/SDL2/SDL_TextureAccess 815 | */ 816 | export enum TextureAccess { 817 | Static = 0, 818 | Streaming = 1, 819 | Target = 2, 820 | } 821 | 822 | /** 823 | * A class used to create textures. 824 | */ 825 | export class TextureCreator { 826 | constructor(private raw: Deno.PointerValue) {} 827 | 828 | /** 829 | * Create a texture for a rendering context. 830 | * @param format the format of the texture 831 | * @param access one of the enumerated values in TextureAccess or a number 832 | * @param w the width of the texture in pixels 833 | * @param h the height of the texture in pixels 834 | * @returns a Texture object 835 | * 836 | * @example 837 | * ```ts 838 | * const creator = canvas.textureCreator(); 839 | * const texture = creator.createTexture( 840 | * PixelFormat.RGBA8888, 841 | * TextureAccess.Static, 842 | * 640, 843 | * 480, 844 | * ); 845 | * ``` 846 | */ 847 | createTexture( 848 | format: number, 849 | access: number, 850 | w: number, 851 | h: number, 852 | ): Texture { 853 | const raw = sdl2.symbols.SDL_CreateTexture( 854 | this.raw, 855 | format, 856 | access, 857 | w, 858 | h, 859 | ); 860 | if (raw === null) { 861 | throwSDLError(); 862 | } 863 | return new Texture(raw); 864 | } 865 | 866 | /** 867 | * Create a texture from a surface. 868 | * @param surface the surface used to create the texture 869 | * @returns a Texture object 870 | */ 871 | 872 | createTextureFromSurface(surface: Surface): Texture { 873 | const raw = sdl2.symbols.SDL_CreateTextureFromSurface( 874 | this.raw, 875 | surface[_raw], 876 | ); 877 | if (raw === null) { 878 | throwSDLError(); 879 | } 880 | return new Texture(raw); 881 | } 882 | } 883 | 884 | /** 885 | * An interface that contains information about a texture. 886 | */ 887 | export interface TextureQuery { 888 | format: number; 889 | access: TextureAccess; 890 | w: number; 891 | h: number; 892 | } 893 | 894 | /** 895 | * A structure that contains an efficient, driver-specific representation of pixel data. 896 | * @see https://wiki.libsdl.org/SDL2/SDL_Texture 897 | */ 898 | export class Texture { 899 | [_raw]: Deno.PointerValue; 900 | 901 | constructor(private raw: Deno.PointerValue) { 902 | this[_raw] = raw; 903 | } 904 | 905 | /** 906 | * Query the attributes of a texture. 907 | * @returns a TextureQuery 908 | */ 909 | query(): TextureQuery { 910 | const format = new Uint32Array(1); 911 | const access = new Uint32Array(1); 912 | const w = new Uint32Array(1); 913 | const h = new Uint32Array(1); 914 | 915 | const ret = sdl2.symbols.SDL_QueryTexture( 916 | this.raw, 917 | format, 918 | access, 919 | w, 920 | h, 921 | ); 922 | if (ret < 0) { 923 | throwSDLError(); 924 | } 925 | return { 926 | format: format[0], 927 | access: access[0], 928 | w: w[0], 929 | h: h[0], 930 | }; 931 | } 932 | /** 933 | * Set an additional color value multiplied into render copy operations. 934 | * @param r the red color value 935 | * @param g the green color value 936 | * @param b the blue color value 937 | */ 938 | setColorMod(r: number, g: number, b: number) { 939 | const ret = sdl2.symbols.SDL_SetTextureColorMod( 940 | this.raw, 941 | r, 942 | g, 943 | b, 944 | ); 945 | if (ret < 0) { 946 | throwSDLError(); 947 | } 948 | } 949 | /** 950 | * Set an additional alpha value multiplied into render copy operations. 951 | * @param a the source alpha value multiplied into copy operations 952 | */ 953 | setAlphaMod(a: number) { 954 | const ret = sdl2.symbols.SDL_SetTextureAlphaMod(this.raw, a); 955 | if (ret < 0) { 956 | throwSDLError(); 957 | } 958 | } 959 | 960 | /** 961 | * Update the given texture rectangle with new pixel data. 962 | * @param pixels the raw pixel data in the format of the texture 963 | * @param pitch the number of bytes in a row of pixel data, including padding between lines 964 | * @param rect an Rect representing the area to update, or null to update the entire texture 965 | */ 966 | update(pixels: Uint8Array, pitch: number, rect?: Rect) { 967 | const ret = sdl2.symbols.SDL_UpdateTexture( 968 | this.raw, 969 | rect ? rect[_raw] : null, 970 | pixels, 971 | pitch, 972 | ); 973 | if (ret < 0) { 974 | throwSDLError(); 975 | } 976 | } 977 | } 978 | 979 | /** 980 | * A structure that contains the definition of a rectangle, with the origin at the upper left. 981 | * @see https://wiki.libsdl.org/SDL2/SDL_Rect 982 | */ 983 | export class Rect { 984 | [_raw]: Uint32Array; 985 | constructor(x: number, y: number, w: number, h: number) { 986 | this[_raw] = new Uint32Array([x, y, w, h]); 987 | } 988 | /** 989 | * The x coordinate of the rectangle. 990 | */ 991 | get x(): number { 992 | return this[_raw][0]; 993 | } 994 | /** 995 | * The y coordinate of the rectangle. 996 | */ 997 | get y(): number { 998 | return this[_raw][1]; 999 | } 1000 | /** 1001 | * The width of the rectangle. 1002 | */ 1003 | get width(): number { 1004 | return this[_raw][2]; 1005 | } 1006 | /** 1007 | * The height of the rectangle. 1008 | */ 1009 | get height(): number { 1010 | return this[_raw][3]; 1011 | } 1012 | } 1013 | /** 1014 | * A structure that contains a collection of pixels used in software blitting. 1015 | */ 1016 | export class Surface { 1017 | [_raw]: Deno.PointerValue; 1018 | constructor(raw: Deno.PointerValue) { 1019 | this[_raw] = raw; 1020 | } 1021 | 1022 | /** 1023 | * Create a surface from a file. 1024 | * @param path the path to the image file 1025 | * @returns a Surface 1026 | */ 1027 | static fromFile(path: string): Surface { 1028 | if (!sdl2Image) { 1029 | throw new Error("SDL2_image was not loaded"); 1030 | } 1031 | 1032 | const raw = sdl2Image.symbols.IMG_Load(asCString(path)); 1033 | if (raw === null) { 1034 | throwSDLError(); 1035 | } 1036 | return new Surface(raw); 1037 | } 1038 | /** 1039 | * @param path the path to the bmp (bitmap) file 1040 | * @returns a Surface 1041 | */ 1042 | static loadBmp(path: string): Surface { 1043 | if (!sdl2Image) { 1044 | throw new Error("SDL2_image was not loaded"); 1045 | } 1046 | 1047 | const raw = sdl2.symbols.SDL_LoadBMP_RW(asCString(path)); 1048 | if (raw === null) { 1049 | throwSDLError(); 1050 | } 1051 | return new Surface(raw); 1052 | } 1053 | 1054 | static fromRaw(data: Uint8Array): Surface { 1055 | if (!sdl2Image) { 1056 | throw new Error("SDL2_image was not loaded"); 1057 | } 1058 | 1059 | const rwops = sdl2.symbols.SDL_RWFromMem(data, data.byteLength); 1060 | const raw = sdl2Image.symbols.IMG_Load_RW(rwops); 1061 | if (raw === null) { 1062 | throwSDLError(); 1063 | } 1064 | 1065 | return new Surface(raw); 1066 | } 1067 | } 1068 | 1069 | const sizeOfEvent = 56; // type (u32) + event 1070 | const eventBuf = new Uint8Array(sizeOfEvent); 1071 | function makeReader>>( 1072 | eventType: Struct, 1073 | ) { 1074 | return (reader: Deno.UnsafePointerView) => { 1075 | return eventType.read(reader); 1076 | }; 1077 | } 1078 | 1079 | const SDL_QuitEvent = new Struct({ 1080 | type: u32, 1081 | timestamp: u32, 1082 | }); 1083 | 1084 | const SDL_CommonEvent = new Struct({ 1085 | type: u32, 1086 | timestamp: u32, 1087 | }); 1088 | 1089 | const SDL_WindowEvent = new Struct({ 1090 | type: u32, 1091 | timestamp: u32, 1092 | windowID: u32, 1093 | event: u8, 1094 | padding1: u8, 1095 | padding2: u8, 1096 | padding3: u8, 1097 | data1: i32, 1098 | data2: i32, 1099 | }); 1100 | 1101 | const SDL_DisplayEvent = new Struct({ 1102 | type: u32, 1103 | timestamp: u32, 1104 | display: u32, 1105 | event: u8, 1106 | padding1: u8, 1107 | padding2: u8, 1108 | padding3: u8, 1109 | data1: i32, 1110 | data2: i32, 1111 | }); 1112 | 1113 | const SDL_KeySym = new Struct({ 1114 | scancode: u32, 1115 | sym: u32, 1116 | mod: u16, 1117 | unicode: u32, 1118 | }); 1119 | 1120 | const SDL_KeyboardEvent = new Struct({ 1121 | type: u32, 1122 | timestamp: u32, 1123 | windowID: u32, 1124 | state: u8, 1125 | repeat: u8, 1126 | padding2: u8, 1127 | padding3: u8, 1128 | keysym: SDL_KeySym, 1129 | }); 1130 | 1131 | const SDL_MouseMotionEvent = new Struct({ 1132 | type: u32, 1133 | timestamp: u32, 1134 | windowID: u32, 1135 | which: u32, 1136 | state: u32, 1137 | x: i32, 1138 | y: i32, 1139 | xrel: i32, 1140 | yrel: i32, 1141 | }); 1142 | 1143 | const SDL_MouseButtonEvent = new Struct({ 1144 | type: u32, 1145 | timestamp: u32, 1146 | windowID: u32, 1147 | which: u32, 1148 | button: u8, 1149 | state: u8, 1150 | padding1: u8, 1151 | padding2: u8, 1152 | x: i32, 1153 | y: i32, 1154 | }); 1155 | 1156 | const SDL_MouseWheelEvent = new Struct({ 1157 | type: u32, 1158 | timestamp: u32, 1159 | windowID: u32, 1160 | which: u32, 1161 | x: i32, 1162 | y: i32, 1163 | }); 1164 | 1165 | const SDL_AudioDeviceEvent = new Struct({ 1166 | type: u32, 1167 | timestamp: u32, 1168 | which: u32, 1169 | event: u8, 1170 | padding1: u8, 1171 | padding2: u8, 1172 | padding3: u8, 1173 | data1: i32, 1174 | data2: i32, 1175 | }); 1176 | 1177 | const SDL_FirstEvent = new Struct({ 1178 | type: u32, 1179 | }); 1180 | 1181 | const SDL_LastEvent = new Struct({ 1182 | type: u32, 1183 | }); 1184 | 1185 | const SDL_Version = new Struct({ 1186 | major: u8, 1187 | minor: u8, 1188 | patch: u8, 1189 | }); 1190 | 1191 | const SDL_SysWMInfo = new Struct({ 1192 | version: SDL_Version, 1193 | subsystem: u32, 1194 | window: u64, 1195 | }); 1196 | 1197 | const SDL_TextEditingEvent = new Struct({ 1198 | type: u32, 1199 | timestamp: u32, 1200 | windowID: u32, 1201 | // @ts-ignore cstring 1202 | text: cstring, 1203 | start: i32, 1204 | length: i32, 1205 | }); 1206 | 1207 | const SDL_TextInputEvent = new Struct({ 1208 | type: u32, 1209 | timestamp: u32, 1210 | windowID: u32, 1211 | // @ts-ignore cstring 1212 | text: cstring, 1213 | }); 1214 | 1215 | /* bug in byte_type@0.1.7 where SDL_SysWMInfo.size is NaN */ 1216 | const sizeOfSDL_SysWMInfo = 3 + 4 + 8 * 64; 1217 | const wmInfoBuf = new Uint8Array(sizeOfSDL_SysWMInfo); 1218 | 1219 | type Reader = (reader: Deno.UnsafePointerView) => T; 1220 | 1221 | // deno-lint-ignore no-explicit-any 1222 | const eventReader: Record> = { 1223 | [EventType.First]: makeReader(SDL_FirstEvent), 1224 | [EventType.Quit]: makeReader(SDL_QuitEvent), 1225 | [EventType.WindowEvent]: makeReader(SDL_WindowEvent), 1226 | [EventType.AppTerminating]: makeReader(SDL_CommonEvent), 1227 | [EventType.AppLowMemory]: makeReader(SDL_CommonEvent), 1228 | [EventType.AppWillEnterBackground]: makeReader(SDL_CommonEvent), 1229 | [EventType.AppDidEnterBackground]: makeReader(SDL_CommonEvent), 1230 | [EventType.AppWillEnterForeground]: makeReader(SDL_CommonEvent), 1231 | [EventType.AppDidEnterForeground]: makeReader(SDL_CommonEvent), 1232 | // [EventType.Display]: makeReader(SDL_DisplayEvent), 1233 | [EventType.KeyDown]: makeReader(SDL_KeyboardEvent), 1234 | [EventType.KeyUp]: makeReader(SDL_KeyboardEvent), 1235 | [EventType.TextEditing]: makeReader(SDL_TextEditingEvent), 1236 | [EventType.TextInput]: makeReader(SDL_TextInputEvent), 1237 | [EventType.MouseMotion]: makeReader(SDL_MouseMotionEvent), 1238 | [EventType.MouseButtonDown]: makeReader(SDL_MouseButtonEvent), 1239 | [EventType.MouseButtonUp]: makeReader(SDL_MouseButtonEvent), 1240 | [EventType.MouseWheel]: makeReader(SDL_MouseWheelEvent), 1241 | [EventType.AudioDeviceAdded]: makeReader(SDL_AudioDeviceEvent), 1242 | [EventType.AudioDeviceRemoved]: makeReader(SDL_AudioDeviceEvent), 1243 | [EventType.User]: makeReader(SDL_CommonEvent), 1244 | [EventType.Last]: makeReader(SDL_LastEvent), 1245 | // TODO: Unrechable code 1246 | [EventType.Draw]: makeReader(SDL_CommonEvent), 1247 | }; 1248 | 1249 | export function getKeyName(key: number): string { 1250 | const name = sdl2.symbols.SDL_GetKeyName(key); 1251 | const view = new Deno.UnsafePointerView(name!); 1252 | return view.getCString(); 1253 | } 1254 | 1255 | export function startTextInput() { 1256 | sdl2.symbols.SDL_StartTextInput(); 1257 | } 1258 | 1259 | export function stopTextInput() { 1260 | sdl2.symbols.SDL_StopTextInput(); 1261 | } 1262 | 1263 | const systems = ["cocoa", "win32", "x11", "wayland"] as const; 1264 | 1265 | /** 1266 | * A window. 1267 | */ 1268 | export class Window { 1269 | constructor( 1270 | private raw: Deno.PointerValue, 1271 | private metalView: Deno.PointerValue | null, 1272 | ) {} 1273 | 1274 | /** 1275 | * Create a 2D rendering context for a window. 1276 | * @returns a valid rendering context (Canvas) 1277 | */ 1278 | canvas(): Canvas { 1279 | // Hardware accelerated canvas 1280 | const raw = sdl2.symbols.SDL_CreateRenderer(this.raw, -1, 0); 1281 | return new Canvas(this.raw, raw); 1282 | } 1283 | 1284 | raise() { 1285 | sdl2.symbols.SDL_RaiseWindow(this.raw); 1286 | } 1287 | 1288 | serialize(): ArrayBuffer { 1289 | const [surface, p1, p2] = this.#windowSurface(); 1290 | const buf = new BigInt64Array([ 1291 | BigInt(systems.indexOf(surface)), 1292 | Deno.UnsafePointer.value(p1), 1293 | Deno.UnsafePointer.value(p2), 1294 | ]); 1295 | return buf.buffer; 1296 | } 1297 | 1298 | static deserialize( 1299 | data: ArrayBuffer, 1300 | width: number, 1301 | height: number, 1302 | ): Deno.UnsafeWindowSurface { 1303 | const [surface, p1, p2] = new BigInt64Array(data); 1304 | return new Deno.UnsafeWindowSurface({ 1305 | system: systems[Number(surface)], 1306 | windowHandle: Deno.UnsafePointer.create(p1), 1307 | displayHandle: Deno.UnsafePointer.create(p2), 1308 | width, 1309 | height, 1310 | }); 1311 | } 1312 | 1313 | /** 1314 | * Return a Deno.UnsafeWindowSurface that can be used 1315 | * with WebGPU. 1316 | */ 1317 | windowSurface(width: number, height: number): Deno.UnsafeWindowSurface { 1318 | const [surface, p1, p2] = this.#windowSurface(); 1319 | return new Deno.UnsafeWindowSurface({ 1320 | system: surface, 1321 | windowHandle: p1, 1322 | displayHandle: p2, 1323 | width, 1324 | height, 1325 | }); 1326 | } 1327 | 1328 | #windowSurface(): [ 1329 | typeof systems[number], 1330 | Deno.PointerValue, 1331 | Deno.PointerValue, 1332 | ] { 1333 | const wm_info = Deno.UnsafePointer.of(wmInfoBuf); 1334 | 1335 | // Initialize the version info. 1336 | sdl2.symbols.SDL_GetVersion(wm_info); 1337 | 1338 | const handle = sdl2.symbols.SDL_GetWindowWMInfo(this.raw, wm_info); 1339 | if (handle == 0) { 1340 | throwSDLError(); 1341 | } 1342 | 1343 | const view = new Deno.UnsafePointerView(wm_info!); 1344 | 1345 | const subsystem = view.getUint32(4); // u32 1346 | 1347 | if (isMacos()) { 1348 | const SDL_SYSWM_COCOA = 4; 1349 | 1350 | const window = view.getPointer(4 + 4)!; // usize 1351 | if (subsystem != SDL_SYSWM_COCOA) { 1352 | throw new Error("Expected SDL_SYSWM_COCOA on macOS"); 1353 | } 1354 | return ["cocoa", window, this.metalView]; 1355 | } 1356 | 1357 | if (isWindows()) { 1358 | const SDL_SYSWM_WINDOWS = 1; 1359 | const SDL_SYSWM_WINRT = 8; 1360 | 1361 | const window = view.getPointer(4 + 4)!; // usize 1362 | if (subsystem == SDL_SYSWM_WINDOWS) { 1363 | const hinstance = view.getPointer(4 + 4 + 8 + 8)!; // usize (gap of 8 bytes) 1364 | return ["win32", window, hinstance]; 1365 | } else if (subsystem == SDL_SYSWM_WINRT) { 1366 | throw new Error("WinRT is not supported"); 1367 | // return new Deno.UnsafeWindowSurface("winrt", window, null); 1368 | } 1369 | throw new Error( 1370 | "Expected SDL_SYSWM_WINRT or SDL_SYSWM_WINDOWS on Windows", 1371 | ); 1372 | } 1373 | 1374 | if (isLinux()) { 1375 | const SDL_SYSWM_X11 = 2; 1376 | const SDL_SYSWM_WAYLAND = 6; 1377 | 1378 | const display = view.getPointer(4 + 4)!; // usize 1379 | if (subsystem == SDL_SYSWM_X11) { 1380 | const window = view.getPointer(4 + 4 + 8)!; // usize 1381 | return ["x11", window, display]; 1382 | } else if (subsystem == SDL_SYSWM_WAYLAND) { 1383 | const surface = view.getPointer(4 + 4 + 8)!; // usize 1384 | return ["wayland", surface, display]; 1385 | } 1386 | throw new Error("Expected SDL_SYSWM_X11 or SDL_SYSWM_WAYLAND on Linux"); 1387 | } 1388 | 1389 | throw new Error("Unsupported platform"); 1390 | } 1391 | 1392 | /** 1393 | * Events from the window. 1394 | */ 1395 | // deno-lint-ignore no-explicit-any 1396 | async *events(wait = false): AsyncGenerator { 1397 | while (true) { 1398 | const event = Deno.UnsafePointer.of(eventBuf); 1399 | 1400 | const shouldWait = wait || 1401 | sdl2.symbols.SDL_GetWindowFlags(this.raw) & 1402 | (WINDOW_FLAGS.MINIMIZED | WINDOW_FLAGS.HIDDEN); 1403 | const pending = (shouldWait 1404 | ? sdl2.symbols.SDL_WaitEvent(event) 1405 | : sdl2.symbols.SDL_PollEvent(event)) == 1; 1406 | if (shouldWait) { 1407 | // Run microtasks. 1408 | await new Promise((resolve) => 1409 | setTimeout(resolve, 0) 1410 | ); 1411 | } 1412 | if (!pending) { 1413 | yield { type: EventType.Draw }; 1414 | } 1415 | const view = new Deno.UnsafePointerView(event!); 1416 | const type = view.getUint32(); 1417 | const ev = eventReader[type as EventType]; 1418 | if (!ev) { 1419 | // throw new Error(`Unknown event type: ${type}`); 1420 | continue; 1421 | } 1422 | yield { ...ev(view) }; 1423 | } 1424 | } 1425 | 1426 | [Symbol.dispose]() { 1427 | sdl2.symbols.SDL_DestroyWindow(this.raw); 1428 | } 1429 | } 1430 | 1431 | // Copied from https://github.com/libsdl-org/SDL/blob/main/include/SDL3/SDL_video.h#L129 on 6/13/2023 1432 | enum WINDOW_FLAGS { 1433 | FULLSCREEN = 0x00000001, /**< window is in fullscreen mode */ 1434 | OPENGL = 0x00000002, /**< window usable with OpenGL context */ 1435 | HIDDEN = 0x00000008, /**< window is not visible */ 1436 | BORDERLESS = 0x00000010, /**< no window decoration */ 1437 | RESIZABLE = 0x00000020, /**< window can be resized */ 1438 | MINIMIZED = 0x00000040, /**< window is minimized */ 1439 | MAXIMIZED = 0x00000080, /**< window is maximized */ 1440 | MOUSE_GRABBED = 0x00000100, /**< window has grabbed mouse input */ 1441 | INPUT_FOCUS = 0x00000200, /**< window has input focus */ 1442 | MOUSE_FOCUS = 0x00000400, /**< window has mouse focus */ 1443 | FOREIGN = 0x00000800, /**< window not created by SDL */ 1444 | HIGH_PIXEL_DENSITY = 1445 | 0x00002000, /**< window uses high pixel density back buffer if possible */ 1446 | MOUSE_CAPTURE = 1447 | 0x00004000, /**< window has mouse captured (unrelated to MOUSE_GRABBED) */ 1448 | ALWAYS_ON_TOP = 0x00008000, /**< window should always be above others */ 1449 | SKIP_TASKBAR = 0x00010000, /**< window should not be added to the taskbar */ 1450 | UTILITY = 0x00020000, /**< window should be treated as a utility window */ 1451 | TOOLTIP = 0x00040000, /**< window should be treated as a tooltip */ 1452 | POPUP_MENU = 0x00080000, /**< window should be treated as a popup menu */ 1453 | KEYBOARD_GRABBED = 0x00100000, /**< window has grabbed keyboard input */ 1454 | VULKAN = 0x10000000, /**< window usable for Vulkan surface */ 1455 | METAL = 0x20000000, /**< window usable for Metal view */ 1456 | TRANSPARENT = 0x40000000, /**< window with transparent buffer */ 1457 | } 1458 | 1459 | /** 1460 | * A window builder to create a window. 1461 | * @example 1462 | * ```ts 1463 | * const window = new WindowBuilder("Hello World", 800, 600); 1464 | * ``` 1465 | */ 1466 | export class WindowBuilder { 1467 | private flags: number = 0; 1468 | constructor( 1469 | private title: string, 1470 | private width: number, 1471 | private height: number, 1472 | ) {} 1473 | 1474 | /** 1475 | * Build a window. 1476 | * @returns a window 1477 | */ 1478 | build(): Window { 1479 | const title = asCString(this.title); 1480 | const window = sdl2.symbols.SDL_CreateWindow( 1481 | title, 1482 | 0x2FFF0000, 1483 | 0x2FFF0000, 1484 | this.width, 1485 | this.height, 1486 | this.flags, 1487 | ); 1488 | 1489 | if (window === null) { 1490 | throwSDLError(); 1491 | } 1492 | 1493 | const metal_view = isMacos() 1494 | ? sdl2.symbols.SDL_Metal_CreateView(window) 1495 | : null; 1496 | return new Window(window, metal_view); 1497 | } 1498 | 1499 | /** 1500 | * Set the window to be fullscreen. 1501 | */ 1502 | fullscreen(): WindowBuilder { 1503 | this.flags |= WINDOW_FLAGS.FULLSCREEN; 1504 | return this; 1505 | } 1506 | 1507 | /** 1508 | * Window usable with an OpenGL context 1509 | */ 1510 | opengl(): WindowBuilder { 1511 | this.flags |= WINDOW_FLAGS.OPENGL; 1512 | return this; 1513 | } 1514 | 1515 | /** window is not visible */ 1516 | hidden(): WindowBuilder { 1517 | this.flags |= WINDOW_FLAGS.HIDDEN; 1518 | return this; 1519 | } 1520 | 1521 | /** 1522 | * Set the window to be borderless. 1523 | */ 1524 | borderless(): WindowBuilder { 1525 | this.flags |= WINDOW_FLAGS.BORDERLESS; 1526 | return this; 1527 | } 1528 | 1529 | /** 1530 | * Set the window to be resizable. 1531 | */ 1532 | resizable(): WindowBuilder { 1533 | this.flags |= WINDOW_FLAGS.RESIZABLE; 1534 | return this; 1535 | } 1536 | 1537 | /** window is minimized */ 1538 | minimized(): WindowBuilder { 1539 | this.flags |= WINDOW_FLAGS.MINIMIZED; 1540 | return this; 1541 | } 1542 | 1543 | /** window is maximized */ 1544 | maximized(): WindowBuilder { 1545 | this.flags |= WINDOW_FLAGS.MAXIMIZED; 1546 | return this; 1547 | } 1548 | 1549 | /** window has grabbed mouse input */ 1550 | mouseGrabbed(): WindowBuilder { 1551 | this.flags |= WINDOW_FLAGS.MOUSE_GRABBED; 1552 | return this; 1553 | } 1554 | 1555 | /** window has input focus */ 1556 | inputFocus(): WindowBuilder { 1557 | this.flags |= WINDOW_FLAGS.INPUT_FOCUS; 1558 | return this; 1559 | } 1560 | 1561 | /** window has mouse focus */ 1562 | mouseFocus(): WindowBuilder { 1563 | this.flags |= WINDOW_FLAGS.MOUSE_FOCUS; 1564 | return this; 1565 | } 1566 | 1567 | /** 1568 | * Set the window to be a foreign window. 1569 | */ 1570 | foreign(): WindowBuilder { 1571 | this.flags |= WINDOW_FLAGS.FOREIGN; 1572 | return this; 1573 | } 1574 | 1575 | /** 1576 | * Window should be created in high-DPI mode. 1577 | */ 1578 | highPixelDensity(): WindowBuilder { 1579 | this.flags |= WINDOW_FLAGS.HIGH_PIXEL_DENSITY; 1580 | return this; 1581 | } 1582 | 1583 | /** window has mouse captured (unrelated to MOUSE_GRABBED) */ 1584 | mouseCapture(): WindowBuilder { 1585 | this.flags |= WINDOW_FLAGS.MOUSE_CAPTURE; 1586 | return this; 1587 | } 1588 | 1589 | /** 1590 | * Set the window to be always on top. 1591 | */ 1592 | alwaysOnTop(): WindowBuilder { 1593 | this.flags |= WINDOW_FLAGS.ALWAYS_ON_TOP; 1594 | return this; 1595 | } 1596 | 1597 | /** window should not be added to the taskbar */ 1598 | skipTaskbar(): WindowBuilder { 1599 | this.flags |= WINDOW_FLAGS.SKIP_TASKBAR; 1600 | return this; 1601 | } 1602 | 1603 | /** window should be treated as a utility window */ 1604 | utility(): WindowBuilder { 1605 | this.flags |= WINDOW_FLAGS.UTILITY; 1606 | return this; 1607 | } 1608 | 1609 | /** window should be treated as a tooltip */ 1610 | tooltip(): WindowBuilder { 1611 | this.flags |= WINDOW_FLAGS.TOOLTIP; 1612 | return this; 1613 | } 1614 | 1615 | /** window should be treated as a popup menu */ 1616 | popupMenu(): WindowBuilder { 1617 | this.flags |= WINDOW_FLAGS.POPUP_MENU; 1618 | return this; 1619 | } 1620 | 1621 | /** window has grabbed keyboard input */ 1622 | keyboardGrabbed(): WindowBuilder { 1623 | this.flags |= WINDOW_FLAGS.KEYBOARD_GRABBED; 1624 | return this; 1625 | } 1626 | 1627 | /** window usable for Vulkan surface */ 1628 | vulkan(): WindowBuilder { 1629 | this.flags |= WINDOW_FLAGS.VULKAN; 1630 | return this; 1631 | } 1632 | 1633 | /** window usable for Metal view */ 1634 | metal(): WindowBuilder { 1635 | this.flags |= WINDOW_FLAGS.METAL; 1636 | return this; 1637 | } 1638 | 1639 | /** window with transparent buffer */ 1640 | transparent(): WindowBuilder { 1641 | this.flags |= WINDOW_FLAGS.TRANSPARENT; 1642 | return this; 1643 | } 1644 | } 1645 | /** 1646 | * A video subsystem. 1647 | */ 1648 | export class VideoSubsystem { 1649 | /** 1650 | * Get the name of the currently initialized video driver. 1651 | */ 1652 | currentVideoDriver(): string { 1653 | const buf = sdl2.symbols.SDL_GetCurrentVideoDriver(); 1654 | if (buf === null) { 1655 | throwSDLError(); 1656 | } 1657 | const view = new Deno.UnsafePointerView(buf); 1658 | return view.getCString(); 1659 | } 1660 | } 1661 | -------------------------------------------------------------------------------- /tests/basic.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from "../mod.ts"; 2 | 3 | const canvas = new Canvas({ 4 | title: "Hello, Deno!", 5 | height: 800, 6 | width: 600, 7 | centered: true, 8 | fullscreen: false, 9 | hidden: false, 10 | resizable: true, 11 | minimized: false, 12 | maximized: false, 13 | flags: null, 14 | }); 15 | 16 | canvas.clear(); 17 | canvas.present(); 18 | // Fire up the event loop 19 | for await (const event of canvas) { 20 | if (event.type == "draw") { 21 | canvas.clear(); 22 | canvas.present(); 23 | } 24 | if (event.type == "quit") { 25 | canvas.quit(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/deno_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/tests/deno_logo.png -------------------------------------------------------------------------------- /tests/mp3_flag.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from "../mod.ts"; 2 | 3 | const canvas = new Canvas({ 4 | title: "Hello, Deno!", 5 | height: 800, 6 | width: 600, 7 | centered: true, 8 | fullscreen: false, 9 | hidden: false, 10 | resizable: true, 11 | minimized: false, 12 | maximized: false, 13 | flags: null, 14 | }); 15 | 16 | canvas.playMusic("tests/sample_0.mp3"); 17 | 18 | // Fire up the event loop 19 | for await (const event of canvas) { 20 | if (event.type == "quit") { 21 | canvas.quit(); 22 | } 23 | continue; 24 | } 25 | -------------------------------------------------------------------------------- /tests/sample_0.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littledivy/deno_sdl2/0e85bf1c5c7e591f908c152dc27de53c869b4b67/tests/sample_0.mp3 -------------------------------------------------------------------------------- /tests/sdl2_image_test.ts: -------------------------------------------------------------------------------- 1 | import { EventType, Surface, WindowBuilder } from "../mod.ts"; 2 | 3 | const window = new WindowBuilder("Hello, Deno!", 800, 600).build(); 4 | const canvas = window.canvas(); 5 | 6 | const surface = Surface.fromFile("tests/deno_logo.png"); 7 | 8 | const creator = canvas.textureCreator(); 9 | const texture = creator.createTextureFromSurface(surface); 10 | 11 | // Fire up the event loop 12 | for (const event of canvas.events()) { 13 | if (event.type == EventType.Quit) { 14 | canvas.quit(); 15 | } 16 | 17 | canvas.setDrawColor(255, 255, 255, 255); 18 | canvas.clear(); 19 | canvas.copy(texture, { x: 0, y: 0, width: 290, height: 250 }, { 20 | x: 0, 21 | y: 0, 22 | width: 290, 23 | height: 250, 24 | }); 25 | 26 | canvas.present(); 27 | 28 | continue; 29 | } 30 | -------------------------------------------------------------------------------- /tests/system.ts: -------------------------------------------------------------------------------- 1 | import { VideoSubsystem } from "../mod.ts"; 2 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 3 | 4 | Deno.test("VideoSubsystem#currentVideoDriver", () => { 5 | const video = new VideoSubsystem(); 6 | const info = video.currentVideoDriver(); 7 | assertEquals(typeof info, "string"); 8 | }); 9 | -------------------------------------------------------------------------------- /webgpu-examples/boids/boids.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Canvas, 3 | EventType, 4 | PixelFormat, 5 | Rect, 6 | type Texture, 7 | TextureAccess, 8 | type Window, 9 | WindowBuilder, 10 | } from "../../mod.ts"; 11 | 12 | class Boids { 13 | particleCount: number; 14 | particlesPerGroup: number; 15 | 16 | computePipeline!: GPUComputePipeline; 17 | particleBindGroups: GPUBindGroup[] = []; 18 | renderPipeline!: GPURenderPipeline; 19 | particleBuffers: GPUBuffer[] = []; 20 | verticesBuffer!: GPUBuffer; 21 | 22 | frameNum = 0; 23 | dimensions = { 24 | width: 800, 25 | height: 800, 26 | }; 27 | screenDimensions = { 28 | width: 800, 29 | height: 800, 30 | }; 31 | texture: GPUTexture; 32 | outputBuffer: GPUBuffer; 33 | 34 | canvas: Canvas; 35 | sdl2texture: Texture; 36 | window: Window; 37 | constructor(options: { 38 | particleCount: number; 39 | particlesPerGroup: number; 40 | }, public device: GPUDevice) { 41 | this.particleCount = options.particleCount; 42 | this.particlesPerGroup = options.particlesPerGroup; 43 | const window = new WindowBuilder( 44 | "Hello, Deno!", 45 | this.dimensions.width, 46 | this.dimensions.height, 47 | ).build(); 48 | this.canvas = window.canvas(); 49 | this.window = window; 50 | const creator = this.canvas.textureCreator(); 51 | this.sdl2texture = creator.createTexture( 52 | PixelFormat.ABGR8888, 53 | TextureAccess.Streaming, 54 | this.dimensions.width, 55 | this.dimensions.height, 56 | ); 57 | this.texture = this.device.createTexture({ 58 | label: "Capture", 59 | size: this.dimensions, 60 | format: "rgba8unorm-srgb", 61 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, 62 | }); 63 | const { padded } = getRowPadding(this.dimensions.width); 64 | this.outputBuffer = this.device.createBuffer({ 65 | label: "Capture", 66 | size: padded * this.dimensions.height, 67 | usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, 68 | }); 69 | } 70 | 71 | init() { 72 | const computeShader = this.device.createShaderModule({ 73 | code: Deno.readTextFileSync(new URL("./compute.wgsl", import.meta.url)), 74 | }); 75 | 76 | const drawShader = this.device.createShaderModule({ 77 | code: Deno.readTextFileSync(new URL("./shader.wgsl", import.meta.url)), 78 | }); 79 | 80 | const simParamData = new Float32Array([ 81 | 0.1, // deltaT 82 | 0.2, // rule1Distance 83 | 0.2, // rule2Distance 84 | 0.2, // rule3Distance 85 | 0.7, // rule1Scale 86 | 0.3, // rule2Scale 87 | 0.5, // rule3Scale 88 | ]); 89 | 90 | const simParamBuffer = createBufferInit(this.device, { 91 | label: "Simulation Parameter Buffer", 92 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 93 | contents: simParamData.buffer, 94 | }); 95 | 96 | const computeBindGroupLayout = this.device.createBindGroupLayout({ 97 | entries: [ 98 | { 99 | binding: 0, 100 | visibility: GPUShaderStage.COMPUTE, 101 | buffer: { 102 | minBindingSize: simParamData.length * 4, 103 | }, 104 | }, 105 | { 106 | binding: 1, 107 | visibility: GPUShaderStage.COMPUTE, 108 | buffer: { 109 | type: "read-only-storage", 110 | minBindingSize: this.particleCount * 16, 111 | }, 112 | }, 113 | { 114 | binding: 2, 115 | visibility: GPUShaderStage.COMPUTE, 116 | buffer: { 117 | type: "storage", 118 | minBindingSize: this.particleCount * 16, 119 | }, 120 | }, 121 | ], 122 | }); 123 | const computePipelineLayout = this.device.createPipelineLayout({ 124 | label: "compute", 125 | bindGroupLayouts: [computeBindGroupLayout], 126 | }); 127 | const renderPipelineLayout = this.device.createPipelineLayout({ 128 | label: "render", 129 | bindGroupLayouts: [], 130 | }); 131 | this.renderPipeline = this.device.createRenderPipeline({ 132 | layout: renderPipelineLayout, 133 | vertex: { 134 | module: drawShader, 135 | entryPoint: "main", 136 | buffers: [ 137 | { 138 | arrayStride: 4 * 4, 139 | stepMode: "instance", 140 | attributes: [ 141 | { 142 | format: "float32x2", 143 | offset: 0, 144 | shaderLocation: 0, 145 | }, 146 | { 147 | format: "float32x2", 148 | offset: 8, 149 | shaderLocation: 1, 150 | }, 151 | ], 152 | }, 153 | { 154 | arrayStride: 2 * 4, 155 | attributes: [ 156 | { 157 | format: "float32x2", 158 | offset: 0, 159 | shaderLocation: 2, 160 | }, 161 | ], 162 | }, 163 | ], 164 | }, 165 | fragment: { 166 | module: drawShader, 167 | entryPoint: "frag_main", 168 | targets: [ 169 | { 170 | format: "rgba8unorm-srgb", 171 | }, 172 | ], 173 | }, 174 | }); 175 | this.computePipeline = this.device.createComputePipeline({ 176 | label: "Compute pipeline", 177 | layout: computePipelineLayout, 178 | compute: { 179 | module: computeShader, 180 | entryPoint: "main", 181 | }, 182 | }); 183 | const vertexBufferData = new Float32Array([ 184 | -0.01, 185 | -0.02, 186 | 0.01, 187 | -0.02, 188 | 0.00, 189 | 0.02, 190 | ]); 191 | this.verticesBuffer = createBufferInit(this.device, { 192 | label: "Vertex Buffer", 193 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, 194 | contents: vertexBufferData.buffer, 195 | }); 196 | 197 | const initialParticleData = new Float32Array(4 * this.particleCount); 198 | for (let i = 0; i < initialParticleData.length; i += 4) { 199 | initialParticleData[i] = 2.0 * (Math.random() - 0.5); // posx 200 | initialParticleData[i + 1] = 2.0 * (Math.random() - 0.5); // posy 201 | initialParticleData[i + 2] = 2.0 * (Math.random() - 0.5) * 0.1; // velx 202 | initialParticleData[i + 3] = 2.0 * (Math.random() - 0.5) * 0.1; 203 | } 204 | 205 | for (let i = 0; i < 2; i++) { 206 | this.particleBuffers.push(createBufferInit(this.device, { 207 | label: "Particle Buffer " + i, 208 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | 209 | GPUBufferUsage.COPY_DST, 210 | contents: initialParticleData.buffer, 211 | })); 212 | } 213 | 214 | for (let i = 0; i < 2; i++) { 215 | this.particleBindGroups.push(this.device.createBindGroup({ 216 | layout: computeBindGroupLayout, 217 | entries: [ 218 | { 219 | binding: 0, 220 | resource: { 221 | buffer: simParamBuffer, 222 | }, 223 | }, 224 | { 225 | binding: 1, 226 | resource: { 227 | buffer: this.particleBuffers[i], 228 | }, 229 | }, 230 | { 231 | binding: 2, 232 | resource: { 233 | buffer: this.particleBuffers[(i + 1) % 2], 234 | }, 235 | }, 236 | ], 237 | })); 238 | } 239 | } 240 | 241 | render(encoder: GPUCommandEncoder, view: GPUTextureView) { 242 | encoder.pushDebugGroup("compute boid movement"); 243 | const computePass = encoder.beginComputePass(); 244 | computePass.setPipeline(this.computePipeline); 245 | computePass.setBindGroup(0, this.particleBindGroups[this.frameNum % 2]); 246 | computePass.dispatchWorkgroups( 247 | Math.ceil(this.particleCount / this.particlesPerGroup), 248 | ); 249 | computePass.end(); 250 | encoder.copyBufferToBuffer( 251 | this.particleBuffers[0], 252 | 0, 253 | this.particleBuffers[1], 254 | 0, 255 | 16, 256 | ); 257 | encoder.popDebugGroup(); 258 | 259 | encoder.pushDebugGroup("render boids"); 260 | const renderPass = encoder.beginRenderPass({ 261 | colorAttachments: [ 262 | { 263 | view: view, 264 | loadOp: "clear", 265 | storeOp: "store", 266 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 267 | }, 268 | ], 269 | }); 270 | renderPass.setPipeline(this.renderPipeline); 271 | renderPass.setVertexBuffer( 272 | 0, 273 | this.particleBuffers[(this.frameNum + 1) % 2], 274 | ); 275 | renderPass.setVertexBuffer(1, this.verticesBuffer); 276 | renderPass.draw(3, this.particleCount); 277 | renderPass.end(); 278 | encoder.popDebugGroup(); 279 | 280 | this.frameNum += 1; 281 | } 282 | 283 | async update() { 284 | const encoder = this.device.createCommandEncoder(); 285 | const { padded, unpadded } = getRowPadding(this.dimensions.width); 286 | this.render(encoder, this.texture.createView()); 287 | // const outputBuffer = this.device.createBuffer({ 288 | // label: "Capture", 289 | // size: padded * this.dimensions.height, 290 | // usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, 291 | // }); 292 | encoder.copyTextureToBuffer( 293 | { texture: this.texture }, 294 | { 295 | buffer: this.outputBuffer, 296 | bytesPerRow: padded, 297 | rowsPerImage: 0, 298 | }, 299 | this.dimensions, 300 | ); 301 | this.device.queue.submit([encoder.finish()]); 302 | await this.outputBuffer.mapAsync(1); 303 | const buf = new Uint8Array(this.outputBuffer.getMappedRange()); 304 | const buffer = new Uint8Array(unpadded * this.dimensions.height); 305 | for (let i = 0; i < this.dimensions.height; i++) { 306 | const slice = buf 307 | .slice(i * padded, (i + 1) * padded) 308 | .slice(0, unpadded); 309 | 310 | buffer.set(slice, i * unpadded); 311 | } 312 | this.sdl2texture.update(buffer, this.dimensions.width * 4); 313 | const rect = new Rect(0, 0, this.dimensions.width, this.dimensions.height); 314 | const screen = new Rect( 315 | 0, 316 | 0, 317 | this.screenDimensions.width, 318 | this.screenDimensions.height, 319 | ); 320 | this.canvas.copy(this.sdl2texture, rect, screen); 321 | this.canvas.present(); 322 | this.outputBuffer.unmap(); 323 | } 324 | } 325 | 326 | async function getDevice(features: GPUFeatureName[] = []): Promise { 327 | const adapter = await navigator.gpu.requestAdapter(); 328 | const device = await adapter?.requestDevice({ 329 | requiredFeatures: features, 330 | }); 331 | 332 | if (!device) { 333 | throw new Error("no suitable adapter found"); 334 | } 335 | 336 | return device; 337 | } 338 | 339 | export function createBufferInit( 340 | device: GPUDevice, 341 | descriptor: BufferInit, 342 | ): GPUBuffer { 343 | const contents = new Uint8Array(descriptor.contents); 344 | 345 | const unpaddedSize = contents.byteLength; 346 | const padding = 4 - unpaddedSize % 4; 347 | const paddedSize = padding + unpaddedSize; 348 | 349 | const buffer = device.createBuffer({ 350 | label: descriptor.label, 351 | usage: descriptor.usage, 352 | mappedAtCreation: true, 353 | size: paddedSize, 354 | }); 355 | const data = new Uint8Array(buffer.getMappedRange()); 356 | data.set(contents); 357 | buffer.unmap(); 358 | return buffer; 359 | } 360 | 361 | interface BufferInit { 362 | label?: string; 363 | usage: number; 364 | contents: ArrayBuffer; 365 | } 366 | 367 | function getRowPadding(width: number) { 368 | const bytesPerPixel = 4; 369 | const unpaddedBytesPerRow = width * bytesPerPixel; 370 | const align = 256; 371 | const paddedBytesPerRowPadding = (align - unpaddedBytesPerRow % align) % 372 | align; 373 | const paddedBytesPerRow = unpaddedBytesPerRow + paddedBytesPerRowPadding; 374 | 375 | return { 376 | unpadded: unpaddedBytesPerRow, 377 | padded: paddedBytesPerRow, 378 | }; 379 | } 380 | 381 | const boids = new Boids({ 382 | particleCount: 100, 383 | particlesPerGroup: 64, 384 | }, await getDevice()); 385 | boids.init(); 386 | 387 | async function loop() { 388 | const event = (await boids.window.events().next()).value; 389 | switch (event.type) { 390 | // case EventType.Re: { 391 | // const { width, height } = event; 392 | // boids.canvas.copy(boids.sdl2texture, { x: 0, y: 0, width, height }, { 393 | // x: 0, 394 | // y: 0, 395 | // width, 396 | // height, 397 | // }); 398 | // boids.screenDimensions = { width, height }; 399 | // boids.canvas.present(); 400 | // break; 401 | // } 402 | case EventType.Draw: { 403 | await boids.update(); 404 | break; 405 | } 406 | case EventType.Quit: 407 | case EventType.KeyDown: 408 | Deno.exit(0); 409 | break; 410 | default: 411 | break; 412 | } 413 | 414 | await loop(); 415 | } 416 | 417 | await loop(); 418 | -------------------------------------------------------------------------------- /webgpu-examples/boids/compute.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | pos : vec2, 3 | vel : vec2, 4 | }; 5 | 6 | struct SimParams { 7 | deltaT : f32, 8 | rule1Distance : f32, 9 | rule2Distance : f32, 10 | rule3Distance : f32, 11 | rule1Scale : f32, 12 | rule2Scale : f32, 13 | rule3Scale : f32, 14 | }; 15 | 16 | struct Particles { 17 | particles : array, 18 | }; 19 | 20 | @group(0) @binding(0) var params : SimParams; 21 | @group(0) @binding(1) var particlesSrc : Particles; 22 | @group(0) @binding(2) var particlesDst : Particles; 23 | 24 | // https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp 25 | @compute @workgroup_size(64) 26 | fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { 27 | let total = arrayLength(&particlesSrc.particles); 28 | let index = global_invocation_id.x; 29 | if (index >= total) { 30 | return; 31 | } 32 | 33 | var vPos : vec2 = particlesSrc.particles[index].pos; 34 | var vVel : vec2 = particlesSrc.particles[index].vel; 35 | 36 | var cMass : vec2 = vec2(0.0, 0.0); 37 | var cVel : vec2 = vec2(0.0, 0.0); 38 | var colVel : vec2 = vec2(0.0, 0.0); 39 | var cMassCount : i32 = 0; 40 | var cVelCount : i32 = 0; 41 | 42 | var i : u32 = 0u; 43 | loop { 44 | if (i >= total) { 45 | break; 46 | } 47 | if (i == index) { 48 | continue; 49 | } 50 | 51 | let pos = particlesSrc.particles[i].pos; 52 | let vel = particlesSrc.particles[i].vel; 53 | 54 | if (distance(pos, vPos) < params.rule1Distance) { 55 | cMass = cMass + pos; 56 | cMassCount = cMassCount + 1; 57 | } 58 | if (distance(pos, vPos) < params.rule2Distance) { 59 | colVel = colVel - (pos - vPos); 60 | } 61 | if (distance(pos, vPos) < params.rule3Distance) { 62 | cVel = cVel + vel; 63 | cVelCount = cVelCount + 1; 64 | } 65 | 66 | continuing { 67 | i = i + 1u; 68 | } 69 | } 70 | if (cMassCount > 0) { 71 | cMass = cMass * (1.0 / f32(cMassCount)) - vPos; 72 | } 73 | if (cVelCount > 0) { 74 | cVel = cVel * (1.0 / f32(cVelCount)); 75 | } 76 | 77 | vVel = vVel + (cMass * params.rule1Scale) + 78 | (colVel * params.rule2Scale) + 79 | (cVel * params.rule3Scale); 80 | 81 | // clamp velocity for a more pleasing simulation 82 | vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1); 83 | 84 | // kinematic update 85 | vPos = vPos + (vVel * params.deltaT); 86 | 87 | // Wrap around boundary 88 | if (vPos.x < -1.0) { 89 | vPos.x = 1.0; 90 | } 91 | if (vPos.x > 1.0) { 92 | vPos.x = -1.0; 93 | } 94 | if (vPos.y < -1.0) { 95 | vPos.y = 1.0; 96 | } 97 | if (vPos.y > 1.0) { 98 | vPos.y = -1.0; 99 | } 100 | 101 | // Write back 102 | particlesDst.particles[index].pos = vPos; 103 | particlesDst.particles[index].vel = vVel; 104 | } -------------------------------------------------------------------------------- /webgpu-examples/boids/shader.wgsl: -------------------------------------------------------------------------------- 1 | @vertex 2 | fn main( 3 | @location(0) particle_pos: vec2, 4 | @location(1) particle_vel: vec2, 5 | @location(2) position: vec2, 6 | ) -> @builtin(position) vec4 { 7 | let angle = -atan2(particle_vel.x, particle_vel.y); 8 | let pos = vec2( 9 | position.x * cos(angle) - position.y * sin(angle), 10 | position.x * sin(angle) + position.y * cos(angle) 11 | ); 12 | return vec4(pos + particle_pos, 0.0, 1.0); 13 | } 14 | 15 | @fragment 16 | fn frag_main() -> @location(0) vec4 { 17 | return vec4(1.0, 1.0, 1.0, 1.0); 18 | } --------------------------------------------------------------------------------