Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,63 @@
|
||||
## Description
|
||||
|
||||
`gg` is V's simple graphics module.
|
||||
It is currently implemented using `sokol`, and makes easy creating
|
||||
apps that just need a way to draw simple 2D shapes, and to react to
|
||||
user's keyboard/mouse input.
|
||||
|
||||
## Example
|
||||
|
||||
```v cgen
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
fn main() {
|
||||
mut context := gg.new_context(
|
||||
bg_color: gg.rgb(174, 198, 255)
|
||||
width: 600
|
||||
height: 400
|
||||
window_title: 'Polygons'
|
||||
frame_fn: frame
|
||||
)
|
||||
context.run()
|
||||
}
|
||||
|
||||
fn frame(mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
ctx.draw_convex_poly([f32(100.0), 100.0, 200.0, 100.0, 300.0, 200.0, 200.0, 300.0, 100.0, 300.0],
|
||||
gg.blue)
|
||||
ctx.draw_poly_empty([f32(50.0), 50.0, 70.0, 60.0, 90.0, 80.0, 70.0, 110.0], gg.black)
|
||||
ctx.draw_triangle_filled(450, 142, 530, 280, 370, 280, gg.red)
|
||||
ctx.end()
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
A common problem, if you draw a lot of primitive elements in the same
|
||||
frame, is that there is a chance that your program can exceed the maximum
|
||||
allowed amount of vertices and commands, imposed by `sokol`.
|
||||
The symptom is that your frame will be suddenly black, after it becomes more complex.
|
||||
Sokol's default for vertices is 131072.
|
||||
Sokol's default for commands is 32768.
|
||||
|
||||
To solve that, you can try adding these lines at the top of your program:
|
||||
`#flag -D_SGL_DEFAULT_MAX_VERTICES=4194304`
|
||||
`#flag -D_SGL_DEFAULT_MAX_COMMANDS=65536`
|
||||
You can see an example of that in:
|
||||
https://github.com/vlang/v/blob/master/examples/gg/many_thousands_of_circles_overriding_max_vertices.v
|
||||
|
||||
Another approach is to use several draw passes, and limit the amount
|
||||
of draw calls that you make in each, demonstrated in:
|
||||
https://github.com/vlang/v/blob/master/examples/gg/many_thousands_of_circles.v
|
||||
|
||||
Another approach to that problem, is to draw everything yourself in a streaming
|
||||
texture, then upload that streaming texture as a single draw command to the GPU.
|
||||
You can see an example of that done in:
|
||||
https://github.com/vlang/v/blob/master/examples/gg/random.v
|
||||
and in:
|
||||
https://github.com/vlang/v/blob/master/examples/gg/random_stars.v
|
||||
|
||||
A third approach, is to only upload your changing inputs to the GPU, and do all
|
||||
the calculations and drawing there in shaders.
|
||||
@@ -0,0 +1,57 @@
|
||||
module gg
|
||||
|
||||
import math
|
||||
import sokol.sgl
|
||||
|
||||
// draw_cubic_bezier_recursive draws a cubic Bézier curve, also known as a spline, from four points,
|
||||
// where the first and the last points, *will* be part of the curve, and the middle 2 points are control ones.
|
||||
// Unlike `draw_cubic_bezier_in_steps`, this method does not use a fixed number of steps for the whole curve,
|
||||
// but tries to produce more tesselation points dynamically for the curvier parts.
|
||||
@[direct_array_access]
|
||||
pub fn (ctx &Context) draw_cubic_bezier_recursive(points []f32, c Color) {
|
||||
if points.len < 8 {
|
||||
return
|
||||
}
|
||||
ctx.draw_cubic_bezier_recursive_scalar(points[0], points[1], points[2], points[3], points[4],
|
||||
points[5], points[6], points[7], c)
|
||||
}
|
||||
|
||||
// draw_cubic_bezier_recursive_scalar is the same as `draw_cubic_bezier_recursive`, except that the `points` are given
|
||||
// as indiviual x,y f32 scalar parameters, and not in a single dynamic array parameter.
|
||||
pub fn (ctx &Context) draw_cubic_bezier_recursive_scalar(x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32,
|
||||
x4 f32, y4 f32, c Color) {
|
||||
if c.a == 0 {
|
||||
return
|
||||
}
|
||||
if c.a != 255 {
|
||||
sgl.load_pipeline(ctx.pipeline.alpha)
|
||||
}
|
||||
sgl.c4b(c.r, c.g, c.b, c.a)
|
||||
sgl.begin_line_strip()
|
||||
sgl.v2f(x1 * ctx.scale, y1 * ctx.scale)
|
||||
ctx.cubic_bezier_rec(x1, y1, x2, y2, x3, y3, x4, y4, 0)
|
||||
sgl.v2f(x4 * ctx.scale, y4 * ctx.scale)
|
||||
sgl.end()
|
||||
}
|
||||
|
||||
// based on nsvg__flattenCubicBez, from https://github.com/memononen/nanosvg/ :
|
||||
fn (ctx &Context) cubic_bezier_rec(x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32, x4 f32, y4 f32, level int) {
|
||||
if level > 10 {
|
||||
return
|
||||
}
|
||||
dx, dy := x4 - x1, y4 - y1
|
||||
d2 := math.abs((x2 - x4) * dy - (y2 - y4) * dx)
|
||||
d3 := math.abs((x3 - x4) * dy - (y3 - y4) * dx)
|
||||
if (d2 + d3) * (d2 + d3) < 0.25 * (dx * dx + dy * dy) {
|
||||
sgl.v2f(x4 * ctx.scale, y4 * ctx.scale)
|
||||
return
|
||||
}
|
||||
x12, y12 := 0.5 * (x1 + x2), 0.5 * (y1 + y2)
|
||||
x23, y23 := 0.5 * (x2 + x3), 0.5 * (y2 + y3)
|
||||
x34, y34 := 0.5 * (x3 + x4), 0.5 * (y3 + y4)
|
||||
x234, y234 := 0.5 * (x23 + x34), 0.5 * (y23 + y34)
|
||||
x123, y123 := 0.5 * (x12 + x23), 0.5 * (y12 + y23)
|
||||
x1234, y1234 := 0.5 * (x123 + x234), 0.5 * (y123 + y234)
|
||||
ctx.cubic_bezier_rec(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1)
|
||||
ctx.cubic_bezier_rec(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1)
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
module gg
|
||||
|
||||
pub const black = Color{
|
||||
r: 0
|
||||
g: 0
|
||||
b: 0
|
||||
}
|
||||
pub const gray = Color{
|
||||
r: 128
|
||||
g: 128
|
||||
b: 128
|
||||
}
|
||||
pub const white = Color{
|
||||
r: 255
|
||||
g: 255
|
||||
b: 255
|
||||
}
|
||||
pub const red = Color{
|
||||
r: 255
|
||||
g: 0
|
||||
b: 0
|
||||
}
|
||||
pub const green = Color{
|
||||
r: 0
|
||||
g: 255
|
||||
b: 0
|
||||
}
|
||||
pub const blue = Color{
|
||||
r: 0
|
||||
g: 0
|
||||
b: 255
|
||||
}
|
||||
pub const yellow = Color{
|
||||
r: 255
|
||||
g: 255
|
||||
b: 0
|
||||
}
|
||||
pub const magenta = Color{
|
||||
r: 255
|
||||
g: 0
|
||||
b: 255
|
||||
}
|
||||
pub const cyan = Color{
|
||||
r: 0
|
||||
g: 255
|
||||
b: 255
|
||||
}
|
||||
pub const orange = Color{
|
||||
r: 255
|
||||
g: 165
|
||||
b: 0
|
||||
}
|
||||
pub const purple = Color{
|
||||
r: 128
|
||||
g: 0
|
||||
b: 128
|
||||
}
|
||||
pub const indigo = Color{
|
||||
r: 75
|
||||
g: 0
|
||||
b: 130
|
||||
}
|
||||
pub const pink = Color{
|
||||
r: 255
|
||||
g: 192
|
||||
b: 203
|
||||
}
|
||||
pub const violet = Color{
|
||||
r: 238
|
||||
g: 130
|
||||
b: 238
|
||||
}
|
||||
pub const dark_blue = Color{
|
||||
r: 0
|
||||
g: 0
|
||||
b: 139
|
||||
}
|
||||
pub const dark_gray = Color{
|
||||
r: 169
|
||||
g: 169
|
||||
b: 169
|
||||
}
|
||||
pub const dark_green = Color{
|
||||
r: 0
|
||||
g: 100
|
||||
b: 0
|
||||
}
|
||||
pub const dark_red = Color{
|
||||
r: 139
|
||||
g: 0
|
||||
b: 0
|
||||
}
|
||||
pub const light_blue = Color{
|
||||
r: 173
|
||||
g: 216
|
||||
b: 230
|
||||
}
|
||||
pub const light_gray = Color{
|
||||
r: 211
|
||||
g: 211
|
||||
b: 211
|
||||
}
|
||||
pub const light_green = Color{
|
||||
r: 144
|
||||
g: 238
|
||||
b: 144
|
||||
}
|
||||
pub const light_red = Color{
|
||||
r: 255
|
||||
g: 204
|
||||
b: 203
|
||||
}
|
||||
|
||||
// Color represents a 32 bit color value in sRGB format
|
||||
@[markused]
|
||||
pub struct Color {
|
||||
pub mut:
|
||||
r u8
|
||||
g u8
|
||||
b u8
|
||||
a u8 = 255
|
||||
}
|
||||
|
||||
// hex takes in a 32 bit integer and splits it into 4 byte values
|
||||
pub fn hex(color u32) Color {
|
||||
return Color{
|
||||
r: u8((color >> 16) & 0xFF)
|
||||
g: u8((color >> 8) & 0xFF)
|
||||
b: u8((color >> 0) & 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
// frgb builds a Color instance from the given floating point values (between 0.0 and 1.0) r, g, b
|
||||
@[inline]
|
||||
pub fn frgb[T](r T, g T, b T) Color {
|
||||
return frgba(r, g, b, 1.0)
|
||||
}
|
||||
|
||||
// frgba builds a Color instance from the given floating point values (between 0.0 and 1.0) r, g, b, a
|
||||
@[inline]
|
||||
pub fn frgba[T](r T, g T, b T, a T) Color {
|
||||
return Color{
|
||||
r: u8(r * 255.0)
|
||||
g: u8(g * 255.0)
|
||||
b: u8(b * 255.0)
|
||||
a: u8(a * 255.0)
|
||||
}
|
||||
}
|
||||
|
||||
// rgb builds a Color instance from the given r, g, b u8 values
|
||||
@[inline]
|
||||
pub fn rgb(r u8, g u8, b u8) Color {
|
||||
return Color{
|
||||
r: r
|
||||
g: g
|
||||
b: b
|
||||
}
|
||||
}
|
||||
|
||||
// rgba builds a Color instance from the given r, g, b, a u8 values
|
||||
@[inline]
|
||||
pub fn rgba(r u8, g u8, b u8, a u8) Color {
|
||||
return Color{
|
||||
r: r
|
||||
g: g
|
||||
b: b
|
||||
a: a
|
||||
}
|
||||
}
|
||||
|
||||
// + adds `b` to `a`, with a maximum value of 255 for each channel
|
||||
pub fn (a Color) + (b Color) Color {
|
||||
mut na := i32(a.a) + b.a
|
||||
mut nr := i32(a.r) + b.r
|
||||
mut ng := i32(a.g) + b.g
|
||||
mut nb := i32(a.b) + b.b
|
||||
if na > 255 {
|
||||
na = 255
|
||||
}
|
||||
if nr > 255 {
|
||||
nr = 255
|
||||
}
|
||||
if ng > 255 {
|
||||
ng = 255
|
||||
}
|
||||
if nb > 255 {
|
||||
nb = 255
|
||||
}
|
||||
return Color{
|
||||
r: u8(nr)
|
||||
g: u8(ng)
|
||||
b: u8(nb)
|
||||
a: u8(na)
|
||||
}
|
||||
}
|
||||
|
||||
// - subtracts `b` from `a`, with a minimum value of 0 for each channel
|
||||
// the alpha channel will be set as the minimum between ``a.a`` and ``b.a``
|
||||
pub fn (a Color) - (b Color) Color {
|
||||
mut na := if a.a > b.a { a.a } else { b.a }
|
||||
mut nr := i32(a.r) - b.r
|
||||
mut ng := i32(a.g) - b.g
|
||||
mut nb := i32(a.b) - b.b
|
||||
if na < 0 {
|
||||
na = 0
|
||||
}
|
||||
if nr < 0 {
|
||||
nr = 0
|
||||
}
|
||||
if ng < 0 {
|
||||
ng = 0
|
||||
}
|
||||
if nb < 0 {
|
||||
nb = 0
|
||||
}
|
||||
return Color{
|
||||
r: u8(nr)
|
||||
g: u8(ng)
|
||||
b: u8(nb)
|
||||
a: u8(na)
|
||||
}
|
||||
}
|
||||
|
||||
// * multiplies Color `c` and `c2` keeping channel values in [0, 255] range
|
||||
pub fn (c Color) * (c2 Color) Color {
|
||||
return Color{
|
||||
r: c.r * c2.r
|
||||
g: c.g * c2.g
|
||||
b: c.b * c2.b
|
||||
a: c.a * c2.a
|
||||
}
|
||||
}
|
||||
|
||||
// / divides `c` by `c2` and converts each channel's value to u8(int)
|
||||
pub fn (c Color) / (c2 Color) Color {
|
||||
return Color{
|
||||
r: c.r / c2.r
|
||||
g: c.g / c2.g
|
||||
b: c.b / c2.b
|
||||
a: c.a / c2.a
|
||||
}
|
||||
}
|
||||
|
||||
// over implements an `a` over `b` operation.
|
||||
// see https://keithp.com/~keithp/porterduff/p253-porter.pdf
|
||||
pub fn (a Color) over(b Color) Color {
|
||||
aa := f32(a.a) / 255
|
||||
ab := f32(b.a) / 255
|
||||
ar := aa + ab * (1 - aa)
|
||||
|
||||
rr := (f32(a.r) * aa + f32(b.r) * ab * (1 - aa)) / ar
|
||||
gr := (f32(a.g) * aa + f32(b.g) * ab * (1 - aa)) / ar
|
||||
br := (f32(a.b) * aa + f32(b.b) * ab * (1 - aa)) / ar
|
||||
return Color{
|
||||
r: u8(rr)
|
||||
g: u8(gr)
|
||||
b: u8(br)
|
||||
a: u8(ar * 255)
|
||||
}
|
||||
}
|
||||
|
||||
// eq checks if color `c` and `c2` are equal in every channel
|
||||
pub fn (c Color) eq(c2 Color) bool {
|
||||
return c.r == c2.r && c.g == c2.g && c.b == c2.b && c.a == c2.a
|
||||
}
|
||||
|
||||
// str returns a string representation of the Color `c`
|
||||
pub fn (c Color) str() string {
|
||||
return 'Color{${c.r}, ${c.g}, ${c.b}, ${c.a}}'
|
||||
}
|
||||
|
||||
// rgba8 converts a color value to an 32bit int in the RGBA8 order.
|
||||
// see https://developer.apple.com/documentation/coreimage/ciformat
|
||||
@[inline]
|
||||
pub fn (c Color) rgba8() i32 {
|
||||
return i32(u32(c.r) << 24 | u32(c.g) << 16 | u32(c.b) << 8 | u32(c.a))
|
||||
}
|
||||
|
||||
// bgra8 converts a color value to an 32bit int in the BGRA8 order.
|
||||
// see https://developer.apple.com/documentation/coreimage/ciformat
|
||||
@[inline]
|
||||
pub fn (c Color) bgra8() i32 {
|
||||
return i32(u32(c.b) << 24 | u32(c.g) << 16 | u32(c.r) << 8 | u32(c.a))
|
||||
}
|
||||
|
||||
// abgr8 converts a color value to an 32bit int in the ABGR8 order.
|
||||
// see https://developer.apple.com/documentation/coreimage/ciformat
|
||||
@[inline]
|
||||
pub fn (c Color) abgr8() i32 {
|
||||
return i32(u32(c.a) << 24 | u32(c.b) << 16 | u32(c.g) << 8 | u32(c.r))
|
||||
}
|
||||
|
||||
const string_colors = {
|
||||
'blue': blue
|
||||
'red': red
|
||||
'green': green
|
||||
'yellow': yellow
|
||||
'orange': orange
|
||||
'purple': purple
|
||||
'black': black
|
||||
'gray': gray
|
||||
'indigo': indigo
|
||||
'pink': pink
|
||||
'violet': violet
|
||||
'white': white
|
||||
'dark_blue': dark_blue
|
||||
'dark_gray': dark_gray
|
||||
'dark_green': dark_green
|
||||
'dark_red': dark_red
|
||||
'light_blue': light_blue
|
||||
'light_gray': light_gray
|
||||
'light_green': light_green
|
||||
'light_red': light_red
|
||||
}
|
||||
|
||||
// color_from_string returns a Color, corresponding to the given string
|
||||
// or black Color if string is not found in lookup table, or a hex color if starting with #
|
||||
pub fn color_from_string(s string) Color {
|
||||
if s.starts_with('#') {
|
||||
mut hex_str := '0x' + s[1..]
|
||||
return hex(hex_str.u32())
|
||||
} else {
|
||||
return string_colors[s]
|
||||
}
|
||||
}
|
||||
|
||||
// to_css_string returns a CSS compatible string e.g. `rgba(10,11,12,13)` of the color `c`.
|
||||
pub fn (c Color) to_css_string() string {
|
||||
return 'rgba(${c.r},${c.g},${c.b},${c.a})'
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// vtest build: !musl? // gx now transitively imports sokol, and that needs GL/gl.h, which is not installed on the musl CIs
|
||||
import gg
|
||||
|
||||
fn test_hex() {
|
||||
// valid colors
|
||||
// a := gg.hex(0x6c5ce7ff)
|
||||
// b := gg.rgba(108, 92, 231, 255)
|
||||
assert gg.hex(0xff6600) == gg.rgb(255, 102, 0) // orange
|
||||
// doesn't give right value with short hex value
|
||||
short := gg.hex(0xfff)
|
||||
assert short != gg.white
|
||||
// assert short == gg.Color{0, 0, 15, 255}
|
||||
}
|
||||
|
||||
fn test_add() {
|
||||
a := gg.rgba(100, 100, 100, 100)
|
||||
b := gg.rgba(100, 100, 100, 100)
|
||||
r := gg.rgba(200, 200, 200, 200)
|
||||
assert (a + b) == r
|
||||
|
||||
assert gg.red + gg.green == gg.yellow
|
||||
assert gg.red + gg.blue == gg.magenta
|
||||
assert gg.green + gg.blue == gg.cyan
|
||||
}
|
||||
|
||||
fn test_sub() {
|
||||
a := gg.rgba(100, 100, 100, 50)
|
||||
b := gg.rgba(100, 100, 100, 100)
|
||||
r := gg.rgba(0, 0, 0, 100)
|
||||
assert (a - b) == r
|
||||
|
||||
assert gg.white - gg.green == gg.magenta
|
||||
assert gg.white - gg.blue == gg.yellow
|
||||
assert gg.white - gg.red == gg.cyan
|
||||
}
|
||||
|
||||
fn test_mult() {
|
||||
a := gg.rgba(10, 10, 10, 10)
|
||||
b := gg.rgba(10, 10, 10, 10)
|
||||
r := gg.rgba(100, 100, 100, 100)
|
||||
assert (a * b) == r
|
||||
}
|
||||
|
||||
fn test_div() {
|
||||
a := gg.rgba(100, 100, 100, 100)
|
||||
b := gg.rgba(10, 10, 10, 10)
|
||||
r := gg.rgba(10, 10, 10, 10)
|
||||
assert (a / b) == r
|
||||
}
|
||||
|
||||
fn test_rgba8() {
|
||||
assert gg.white.rgba8() == -1
|
||||
assert gg.black.rgba8() == 255
|
||||
assert gg.red.rgba8() == -16776961
|
||||
assert gg.green.rgba8() == 16711935
|
||||
assert gg.blue.rgba8() == 65535
|
||||
}
|
||||
|
||||
fn test_bgra8() {
|
||||
assert gg.white.bgra8() == -1
|
||||
assert gg.black.bgra8() == 255
|
||||
assert gg.red.bgra8() == 65535
|
||||
assert gg.green.bgra8() == 16711935
|
||||
assert gg.blue.bgra8() == -16776961
|
||||
}
|
||||
|
||||
fn test_abgr8() {
|
||||
assert gg.white.abgr8() == -1
|
||||
assert gg.black.abgr8() == -16777216
|
||||
assert gg.red.abgr8() == -16776961
|
||||
assert gg.green.abgr8() == -16711936
|
||||
assert gg.blue.abgr8() == -65536
|
||||
}
|
||||
|
||||
fn test_over() {
|
||||
// shorten names:
|
||||
r := gg.red
|
||||
g := gg.green
|
||||
b := gg.blue
|
||||
y := gg.yellow
|
||||
semi_r := gg.Color{255, 0, 0, 128}
|
||||
semi_g := gg.Color{0, 255, 0, 128}
|
||||
semi_b := gg.Color{0, 0, 255, 128}
|
||||
// fully opaque colors, should be preserved when laid *over* any others:
|
||||
assert b.over(g) == b
|
||||
assert r.over(g) == r
|
||||
assert y.over(r) == y
|
||||
assert g.over(r) == g
|
||||
|
||||
// half transparent pure colors, *over* pure colors, should preserve them correspondingly:
|
||||
assert semi_r.over(r) == gg.Color{255, 0, 0, 255} // preserve pure red
|
||||
assert semi_r.over(g) == gg.Color{128, 126, 0, 255}
|
||||
assert semi_r.over(b) == gg.Color{128, 0, 126, 255}
|
||||
|
||||
assert semi_g.over(r) == gg.Color{126, 128, 0, 255}
|
||||
assert semi_g.over(g) == gg.Color{0, 255, 0, 255} // preserve pure green
|
||||
assert semi_g.over(b) == gg.Color{0, 128, 126, 255}
|
||||
|
||||
assert semi_b.over(r) == gg.Color{126, 0, 128, 255}
|
||||
assert semi_b.over(g) == gg.Color{0, 126, 128, 255}
|
||||
assert semi_b.over(b) == gg.Color{0, 0, 255, 255} // preserve pure blue
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
// vtest build: !musl? && !sanitized_job?
|
||||
import os
|
||||
|
||||
fn test_all_samples_can_be_compiled() {
|
||||
vexe := @VEXE
|
||||
vroot := os.dir(vexe)
|
||||
samples := os.walk_ext('${vroot}/vlib/gg/testdata', '.vv')
|
||||
mut fails := []string{}
|
||||
for program_source in samples {
|
||||
compile_cmd := '${os.quoted_path(vexe)} ${os.quoted_path(program_source)}'
|
||||
res := os.execute(compile_cmd)
|
||||
if res.exit_code != 0 {
|
||||
eprintln('>>> FAIL ${compile_cmd}')
|
||||
eprintln(res.output)
|
||||
fails << compile_cmd
|
||||
}
|
||||
println('OK ${compile_cmd}')
|
||||
}
|
||||
if fails.len > 0 {
|
||||
eprintln('> Failed summary:')
|
||||
for f in fails {
|
||||
eprintln(' failed cmd: ${f}')
|
||||
}
|
||||
assert false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
module gg
|
||||
|
||||
import math
|
||||
|
||||
const rect_empty_bounds_tolerance = f32(0.001)
|
||||
|
||||
fn rect_empty_bounds_match(a f32, b f32) bool {
|
||||
return math.abs(a - b) <= rect_empty_bounds_tolerance
|
||||
}
|
||||
|
||||
fn test_rect_empty_screen_bounds_keep_the_top_left_corner_aligned() {
|
||||
tleft_x, tleft_y, bright_x, bright_y := rect_empty_screen_bounds(1.0, 12, 34, 56, 78)
|
||||
assert rect_empty_bounds_match(tleft_x, 12.1)
|
||||
assert rect_empty_bounds_match(tleft_y, 34.1)
|
||||
assert rect_empty_bounds_match(bright_x, 67.9)
|
||||
assert rect_empty_bounds_match(bright_y, 111.9)
|
||||
}
|
||||
|
||||
fn test_rect_empty_screen_bounds_apply_scale_before_the_pixel_offsets() {
|
||||
tleft_x, tleft_y, bright_x, bright_y := rect_empty_screen_bounds(2.0, 3, 4, 5, 6)
|
||||
assert rect_empty_bounds_match(tleft_x, 6.1)
|
||||
assert rect_empty_bounds_match(tleft_y, 8.1)
|
||||
assert rect_empty_bounds_match(bright_x, 15.9)
|
||||
assert rect_empty_bounds_match(bright_y, 19.9)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module gg
|
||||
|
||||
pub enum MouseButton {
|
||||
left = 0
|
||||
right = 1
|
||||
middle = 2
|
||||
invalid = 256
|
||||
}
|
||||
|
||||
// Note: unlike the MouseButton enum from above,
|
||||
// the [flag]-ed enum here can have combined states,
|
||||
// representing several pressed buttons at once.
|
||||
|
||||
@[flag]
|
||||
pub enum MouseButtons {
|
||||
left
|
||||
right
|
||||
middle
|
||||
}
|
||||
|
||||
@[flag]
|
||||
pub enum Modifier {
|
||||
shift // (1<<0)
|
||||
ctrl // (1<<1)
|
||||
alt // (1<<2)
|
||||
super // (1<<3)
|
||||
}
|
||||
|
||||
pub enum PenLineType {
|
||||
solid
|
||||
dashed
|
||||
dotted
|
||||
}
|
||||
|
||||
pub enum KeyCode {
|
||||
invalid = 0
|
||||
space = 32
|
||||
apostrophe = 39 //'
|
||||
comma = 44 //,
|
||||
minus = 45 //-
|
||||
period = 46 //.
|
||||
slash = 47 ///
|
||||
_0 = 48
|
||||
_1 = 49
|
||||
_2 = 50
|
||||
_3 = 51
|
||||
_4 = 52
|
||||
_5 = 53
|
||||
_6 = 54
|
||||
_7 = 55
|
||||
_8 = 56
|
||||
_9 = 57
|
||||
semicolon = 59 //;
|
||||
equal = 61 //=
|
||||
a = 65
|
||||
b = 66
|
||||
c = 67
|
||||
d = 68
|
||||
e = 69
|
||||
f = 70
|
||||
g = 71
|
||||
h = 72
|
||||
i = 73
|
||||
j = 74
|
||||
k = 75
|
||||
l = 76
|
||||
m = 77
|
||||
n = 78
|
||||
o = 79
|
||||
p = 80
|
||||
q = 81
|
||||
r = 82
|
||||
s = 83
|
||||
t = 84
|
||||
u = 85
|
||||
v = 86
|
||||
w = 87
|
||||
x = 88
|
||||
y = 89
|
||||
z = 90
|
||||
left_bracket = 91 //[
|
||||
backslash = 92 //\
|
||||
right_bracket = 93 //]
|
||||
grave_accent = 96 //`
|
||||
world_1 = 161 // non-us #1
|
||||
world_2 = 162 // non-us #2
|
||||
escape = 256
|
||||
enter = 257
|
||||
tab = 258
|
||||
backspace = 259
|
||||
insert = 260
|
||||
delete = 261
|
||||
right = 262
|
||||
left = 263
|
||||
down = 264
|
||||
up = 265
|
||||
page_up = 266
|
||||
page_down = 267
|
||||
home = 268
|
||||
end = 269
|
||||
caps_lock = 280
|
||||
scroll_lock = 281
|
||||
num_lock = 282
|
||||
print_screen = 283
|
||||
pause = 284
|
||||
f1 = 290
|
||||
f2 = 291
|
||||
f3 = 292
|
||||
f4 = 293
|
||||
f5 = 294
|
||||
f6 = 295
|
||||
f7 = 296
|
||||
f8 = 297
|
||||
f9 = 298
|
||||
f10 = 299
|
||||
f11 = 300
|
||||
f12 = 301
|
||||
f13 = 302
|
||||
f14 = 303
|
||||
f15 = 304
|
||||
f16 = 305
|
||||
f17 = 306
|
||||
f18 = 307
|
||||
f19 = 308
|
||||
f20 = 309
|
||||
f21 = 310
|
||||
f22 = 311
|
||||
f23 = 312
|
||||
f24 = 313
|
||||
f25 = 314
|
||||
kp_0 = 320
|
||||
kp_1 = 321
|
||||
kp_2 = 322
|
||||
kp_3 = 323
|
||||
kp_4 = 324
|
||||
kp_5 = 325
|
||||
kp_6 = 326
|
||||
kp_7 = 327
|
||||
kp_8 = 328
|
||||
kp_9 = 329
|
||||
kp_decimal = 330
|
||||
kp_divide = 331
|
||||
kp_multiply = 332
|
||||
kp_subtract = 333
|
||||
kp_add = 334
|
||||
kp_enter = 335
|
||||
kp_equal = 336
|
||||
left_shift = 340
|
||||
left_control = 341
|
||||
left_alt = 342
|
||||
left_super = 343
|
||||
right_shift = 344
|
||||
right_control = 345
|
||||
right_alt = 346
|
||||
right_super = 347
|
||||
menu = 348
|
||||
}
|
||||
|
||||
const key_code_max = 512
|
||||
@@ -0,0 +1,52 @@
|
||||
// vtest build: !docker-ubuntu-musl // needs GL/gl.h
|
||||
module gg
|
||||
|
||||
import encoding.base64
|
||||
import time
|
||||
|
||||
fn test_swap_interval_frame_budget() {
|
||||
assert swap_interval_frame_budget(-1) == time.Duration(0)
|
||||
assert swap_interval_frame_budget(0) == time.Duration(0)
|
||||
assert swap_interval_frame_budget(1) == time.second / 60
|
||||
assert swap_interval_frame_budget(2) == time.second / 30
|
||||
assert swap_interval_frame_budget(3) == time.second / 20
|
||||
}
|
||||
|
||||
fn test_parse_screenshot_frames_ignores_blank_entries() {
|
||||
assert parse_screenshot_frames('') == []u64{}
|
||||
assert parse_screenshot_frames('1, 2,,4 ') == [u64(1), 2, 4]
|
||||
}
|
||||
|
||||
fn test_new_gg_recorder_settings_stdout_defaults_to_first_frame() {
|
||||
settings := new_gg_recorder_settings_from_env({
|
||||
'VGG_SCREENSHOT_OUTPUT': 'stdout'
|
||||
}, '/tmp/minimal_demo')
|
||||
assert settings.screenshot_output == .stdout
|
||||
assert settings.screenshot_frames == [u64(1)]
|
||||
assert settings.stop_at_frame == 1
|
||||
}
|
||||
|
||||
fn test_new_gg_recorder_settings_stdout_uses_last_frame_as_default_stop() {
|
||||
settings := new_gg_recorder_settings_from_env({
|
||||
'VGG_SCREENSHOT_OUTPUT': 'stdout'
|
||||
'VGG_SCREENSHOT_FRAMES': '2, 4'
|
||||
}, '/tmp/minimal_demo')
|
||||
assert settings.screenshot_frames == [u64(2), 4]
|
||||
assert settings.stop_at_frame == 4
|
||||
}
|
||||
|
||||
fn test_new_gg_recorder_settings_preserves_explicit_stop_frame() {
|
||||
settings := new_gg_recorder_settings_from_env({
|
||||
'VGG_SCREENSHOT_OUTPUT': 'stdout'
|
||||
'VGG_SCREENSHOT_FRAMES': '3'
|
||||
'VGG_STOP_AT_FRAME': '9'
|
||||
}, '/tmp/minimal_demo')
|
||||
assert settings.stop_at_frame == 9
|
||||
}
|
||||
|
||||
fn test_screenshot_stdout_payload_contains_expected_marker() {
|
||||
png := [u8(0x89), `P`, `N`, `G`]
|
||||
encoded := base64.encode(png)
|
||||
payload := screenshot_stdout_payload(7, png)
|
||||
assert payload == '${gg_record_stdout_prefix} frame=7 format=png encoding=base64 data=${encoded}'
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,768 @@
|
||||
module gg
|
||||
|
||||
import js.dom
|
||||
|
||||
pub enum DOMEventType {
|
||||
invalid
|
||||
key_down
|
||||
key_up
|
||||
char
|
||||
mouse_down
|
||||
mouse_up
|
||||
mouse_scroll
|
||||
mouse_move
|
||||
mouse_enter
|
||||
mouse_leave
|
||||
touches_began
|
||||
touches_moved
|
||||
touches_ended
|
||||
touches_cancelled
|
||||
resized
|
||||
iconified
|
||||
restored
|
||||
focused
|
||||
unfocused
|
||||
suspended
|
||||
resumed
|
||||
update_cursor
|
||||
quit_requested
|
||||
clipboard_pasted
|
||||
files_dropped
|
||||
num
|
||||
}
|
||||
|
||||
pub struct Event {
|
||||
pub mut:
|
||||
frame_count u64
|
||||
typ DOMEventType
|
||||
key_code KeyCode
|
||||
char_code u32
|
||||
key_repeat bool
|
||||
modifiers u32
|
||||
mouse_button MouseButton
|
||||
mouse_x f32
|
||||
mouse_y f32
|
||||
mouse_dx f32
|
||||
mouse_dy f32
|
||||
scroll_x f32
|
||||
scroll_y f32
|
||||
// todo(playX): add touches API support in js.dom
|
||||
// num_touches int
|
||||
// touches [8]TouchPoint
|
||||
window_width int
|
||||
window_height int
|
||||
framebuffer_width int
|
||||
framebuffer_height int
|
||||
}
|
||||
|
||||
pub enum DOMMouseButton {
|
||||
invalid = -1
|
||||
left = 0
|
||||
right = 1
|
||||
middle = 2
|
||||
}
|
||||
|
||||
pub enum DOMModifier {
|
||||
shift = 1 //(1<<0)
|
||||
ctrl = 2 //(1<<1)
|
||||
alt = 4 //(1<<2)
|
||||
super = 8 //(1<<3)
|
||||
lmb = 0x100
|
||||
rmb = 0x200
|
||||
mmb = 0x400
|
||||
}
|
||||
|
||||
pub enum DOMKeyCode {
|
||||
invalid = 0
|
||||
space = 32
|
||||
apostrophe = 39 //'
|
||||
comma = 44 //,
|
||||
minus = 45 //-
|
||||
period = 46 //.
|
||||
slash = 47 ///
|
||||
_0 = 48
|
||||
_1 = 49
|
||||
_2 = 50
|
||||
_3 = 51
|
||||
_4 = 52
|
||||
_5 = 53
|
||||
_6 = 54
|
||||
_7 = 55
|
||||
_8 = 56
|
||||
_9 = 57
|
||||
semicolon = 59 //;
|
||||
equal = 61 //=
|
||||
a = 65
|
||||
b = 66
|
||||
c = 67
|
||||
d = 68
|
||||
e = 69
|
||||
f = 70
|
||||
g = 71
|
||||
h = 72
|
||||
i = 73
|
||||
j = 74
|
||||
k = 75
|
||||
l = 76
|
||||
m = 77
|
||||
n = 78
|
||||
o = 79
|
||||
p = 80
|
||||
q = 81
|
||||
r = 82
|
||||
s = 83
|
||||
t = 84
|
||||
u = 85
|
||||
v = 86
|
||||
w = 87
|
||||
x = 88
|
||||
y = 89
|
||||
z = 90
|
||||
left_bracket = 91 //[
|
||||
backslash = 92 //\
|
||||
right_bracket = 93 //]
|
||||
grave_accent = 96 //`
|
||||
world_1 = 161 // non-us #1
|
||||
world_2 = 162 // non-us #2
|
||||
escape = 256
|
||||
enter = 257
|
||||
tab = 258
|
||||
backspace = 259
|
||||
insert = 260
|
||||
delete = 261
|
||||
right = 262
|
||||
left = 263
|
||||
down = 264
|
||||
up = 265
|
||||
page_up = 266
|
||||
page_down = 267
|
||||
home = 268
|
||||
end = 269
|
||||
caps_lock = 280
|
||||
scroll_lock = 281
|
||||
num_lock = 282
|
||||
print_screen = 283
|
||||
pause = 284
|
||||
f1 = 290
|
||||
f2 = 291
|
||||
f3 = 292
|
||||
f4 = 293
|
||||
f5 = 294
|
||||
f6 = 295
|
||||
f7 = 296
|
||||
f8 = 297
|
||||
f9 = 298
|
||||
f10 = 299
|
||||
f11 = 300
|
||||
f12 = 301
|
||||
f13 = 302
|
||||
f14 = 303
|
||||
f15 = 304
|
||||
f16 = 305
|
||||
f17 = 306
|
||||
f18 = 307
|
||||
f19 = 308
|
||||
f20 = 309
|
||||
f21 = 310
|
||||
f22 = 311
|
||||
f23 = 312
|
||||
f24 = 313
|
||||
f25 = 314
|
||||
kp_0 = 320
|
||||
kp_1 = 321
|
||||
kp_2 = 322
|
||||
kp_3 = 323
|
||||
kp_4 = 324
|
||||
kp_5 = 325
|
||||
kp_6 = 326
|
||||
kp_7 = 327
|
||||
kp_8 = 328
|
||||
kp_9 = 329
|
||||
kp_decimal = 330
|
||||
kp_divide = 331
|
||||
kp_multiply = 332
|
||||
kp_subtract = 333
|
||||
kp_add = 334
|
||||
kp_enter = 335
|
||||
kp_equal = 336
|
||||
left_shift = 340
|
||||
left_control = 341
|
||||
left_alt = 342
|
||||
left_super = 343
|
||||
right_shift = 344
|
||||
right_control = 345
|
||||
right_alt = 346
|
||||
right_super = 347
|
||||
menu = 348
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub:
|
||||
width int
|
||||
height int
|
||||
retina bool
|
||||
resizable bool
|
||||
user_data voidptr
|
||||
font_size int
|
||||
create_window bool
|
||||
// window_user_ptr voidptr
|
||||
window_title string
|
||||
borderless_window bool
|
||||
always_on_top bool
|
||||
bg_color Color
|
||||
init_fn FNCb = unsafe { nil }
|
||||
frame_fn FNCb = unsafe { nil }
|
||||
native_frame_fn FNCb = unsafe { nil }
|
||||
cleanup_fn FNCb = unsafe { nil }
|
||||
fail_fn FNFail = unsafe { nil }
|
||||
|
||||
event_fn FNEvent = unsafe { nil }
|
||||
quit_fn FNEvent = unsafe { nil }
|
||||
|
||||
keydown_fn FNKeyDown = unsafe { nil }
|
||||
keyup_fn FNKeyUp = unsafe { nil }
|
||||
char_fn FNChar = unsafe { nil }
|
||||
|
||||
move_fn FNMove = unsafe { nil }
|
||||
click_fn FNClick = unsafe { nil }
|
||||
unclick_fn FNUnClick = unsafe { nil }
|
||||
leave_fn FNEvent = unsafe { nil }
|
||||
enter_fn FNEvent = unsafe { nil }
|
||||
resized_fn FNEvent = unsafe { nil }
|
||||
scroll_fn FNEvent = unsafe { nil }
|
||||
// wait_events bool // set this to true for UIs, to save power
|
||||
fullscreen bool
|
||||
scale f32 = 1.0
|
||||
sample_count int
|
||||
texture_filter TextureFilter = .linear
|
||||
swap_interval int = 1 // 1 = 60fps, 2 = 30fps etc. Ignored by the JS backend; frame pacing follows requestAnimationFrame.
|
||||
// ved needs this
|
||||
// init_text bool
|
||||
font_path string
|
||||
custom_bold_font_path string
|
||||
ui_mode bool // refreshes only on events to save CPU usage
|
||||
// font bytes for embedding
|
||||
font_bytes_normal []u8
|
||||
font_bytes_bold []u8
|
||||
font_bytes_mono []u8
|
||||
font_bytes_italic []u8
|
||||
native_rendering bool // Cocoa on macOS/iOS, GDI+ on Windows
|
||||
// drag&drop
|
||||
enable_dragndrop bool // enable file dropping (drag'n'drop), default is false
|
||||
max_dropped_files int = 1 // max number of dropped files to process (default: 1)
|
||||
max_dropped_file_path_length int = 2048 // max length in bytes of a dropped UTF-8 file path (default: 2048)
|
||||
html5_canvas_name string = 'canvas' // the id/name of the canvas element, that will be used to render GG apps
|
||||
}
|
||||
|
||||
const size = Size{0, 0}
|
||||
|
||||
pub fn window_size() Size {
|
||||
return size
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
mut:
|
||||
render_text bool = true
|
||||
image_cache []Image
|
||||
needs_refresh bool = true
|
||||
ticks int
|
||||
pub mut:
|
||||
scale f32 = 1.0
|
||||
width int
|
||||
height int
|
||||
window JS.Window @[noinit]
|
||||
config Config
|
||||
user_data voidptr
|
||||
ui_mode bool
|
||||
frame u64
|
||||
mbtn_mask u8
|
||||
mouse_buttons MouseButtons
|
||||
mouse_pos_x int
|
||||
mouse_pos_y int
|
||||
mouse_dx int
|
||||
mouse_dy int
|
||||
scroll_x int
|
||||
scroll_y int
|
||||
|
||||
key_modifiers Modifier // the current key modifiers
|
||||
key_repeat bool // whether the pressed key was an autorepeated one
|
||||
pressed_keys [key_code_max]bool // an array representing all currently pressed keys
|
||||
pressed_keys_edge [key_code_max]bool // true when the previous state of pressed_keys,
|
||||
context JS.CanvasRenderingContext2D @[noinit]
|
||||
canvas JS.HTMLCanvasElement @[noinit]
|
||||
// *before* the current event was different
|
||||
}
|
||||
|
||||
fn get_canvas(elem JS.HTMLElement) JS.HTMLCanvasElement {
|
||||
match elem {
|
||||
JS.HTMLCanvasElement {
|
||||
return elem
|
||||
}
|
||||
else {
|
||||
panic('gg: element is not an HTMLCanvasElement')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_context(canvas JS.HTMLCanvasElement) JS.CanvasRenderingContext2D {
|
||||
ctx := canvas.getContext('2d'.str, js_undefined()) or { panic('cannot get context') }
|
||||
match ctx {
|
||||
JS.CanvasRenderingContext2D {
|
||||
return ctx
|
||||
}
|
||||
else {
|
||||
panic('failed to get 2D context')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_context(cfg Config) &Context {
|
||||
mut g := &Context{}
|
||||
|
||||
g.user_data = cfg.user_data
|
||||
g.width = cfg.width
|
||||
g.height = cfg.height
|
||||
g.ui_mode = cfg.ui_mode
|
||||
mut sz := size
|
||||
sz.height = g.height
|
||||
sz.width = g.width
|
||||
g.config = cfg
|
||||
g.window = dom.window()
|
||||
document := dom.document
|
||||
canvas_elem := document.getElementById(cfg.html5_canvas_name.str) or {
|
||||
panic('gg: cannot get canvas element from cfg.html5_canvas_name.str')
|
||||
}
|
||||
canvas := get_canvas(canvas_elem)
|
||||
g.canvas = canvas
|
||||
g.context = get_context(g.canvas)
|
||||
|
||||
mouse_down_event_handler := fn [mut g] (event JS.Event) {
|
||||
match event {
|
||||
JS.MouseEvent {
|
||||
e := g.handle_mouse_event(event, .mouse_down)
|
||||
if !isnil(g.config.event_fn) {
|
||||
f := g.config.event_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
if !isnil(g.config.click_fn) {
|
||||
f := g.config.click_fn
|
||||
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
mouse_up_event_handler := fn [mut g] (event JS.Event) {
|
||||
match event {
|
||||
JS.MouseEvent {
|
||||
e := g.handle_mouse_event(event, .mouse_up)
|
||||
if !isnil(g.config.event_fn) {
|
||||
f := g.config.event_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
if !isnil(g.config.unclick_fn) {
|
||||
f := g.config.unclick_fn
|
||||
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
mouse_move_event_handler := fn [mut g] (event JS.Event) {
|
||||
match event {
|
||||
JS.MouseEvent {
|
||||
e := g.handle_mouse_event(event, .mouse_move)
|
||||
if !isnil(g.config.event_fn) {
|
||||
f := g.config.event_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
if !isnil(g.config.move_fn) {
|
||||
f := g.config.move_fn
|
||||
f(e.mouse_x, e.mouse_y, g.config.user_data)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
mouse_leave_event_handler := fn [mut g] (event JS.Event) {
|
||||
match event {
|
||||
JS.MouseEvent {
|
||||
e := g.handle_mouse_event(event, .mouse_leave)
|
||||
if !isnil(g.config.event_fn) {
|
||||
f := g.config.event_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
if !isnil(g.config.leave_fn) {
|
||||
f := g.config.leave_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
mouse_enter_event_handler := fn [mut g] (event JS.Event) {
|
||||
match event {
|
||||
JS.MouseEvent {
|
||||
e := g.handle_mouse_event(event, .mouse_enter)
|
||||
if !isnil(g.config.event_fn) {
|
||||
f := g.config.event_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
if !isnil(g.config.enter_fn) {
|
||||
f := g.config.enter_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
keydown_event_handler := fn [mut g] (event JS.Event) {
|
||||
println('keyboard')
|
||||
match event {
|
||||
JS.KeyboardEvent {
|
||||
e := g.handle_keyboard_event(event, .key_down)
|
||||
|
||||
if !isnil(g.config.event_fn) {
|
||||
f := g.config.event_fn
|
||||
f(e, g.config.user_data)
|
||||
}
|
||||
if !isnil(g.config.keydown_fn) {
|
||||
f := g.config.keydown_fn
|
||||
// todo: modifiers
|
||||
f(e.key_code, .super, g.config.user_data)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
g.canvas.addEventListener('mousedown'.str, mouse_down_event_handler, JS.EventListenerOptions{})
|
||||
dom.window().addEventListener('mouseup'.str, mouse_up_event_handler, JS.EventListenerOptions{})
|
||||
g.canvas.addEventListener('mousemove'.str, mouse_move_event_handler, JS.EventListenerOptions{})
|
||||
g.canvas.addEventListener('mouseleave'.str, mouse_leave_event_handler, JS.EventListenerOptions{})
|
||||
g.canvas.addEventListener('mouseenter'.str, mouse_enter_event_handler, JS.EventListenerOptions{})
|
||||
dom.document.addEventListener('keydown'.str, keydown_event_handler, JS.EventListenerOptions{})
|
||||
return g
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) run() {
|
||||
// set context late, in case it changed (e.g., due to embedding)
|
||||
if isnil(ctx.user_data) {
|
||||
ctx.user_data = ctx
|
||||
}
|
||||
|
||||
gg_animation_frame_fn(mut ctx)
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) begin() {
|
||||
// ctx.context.beginPath()
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) end() {
|
||||
// ctx.context.closePath()
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) draw_line(x1 f32, y1 f32, x2 f32, y2 f32, c Color) {
|
||||
ctx.context.beginPath()
|
||||
ctx.context.strokeStyle = c.to_css_string().str
|
||||
ctx.context.moveTo(JS.Number(x1), JS.Number(y1))
|
||||
ctx.context.lineTo(JS.Number(x2), JS.Number(y2))
|
||||
ctx.context.stroke()
|
||||
ctx.context.closePath()
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) quit() {
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) draw_rect(x f32, y f32, w f32, h f32, c Color) {
|
||||
ctx.context.beginPath()
|
||||
ctx.context.fillStyle = c.to_css_string().str
|
||||
ctx.context.fillRect(JS.Number(x), JS.Number(y), JS.Number(w), JS.Number(h))
|
||||
ctx.context.closePath()
|
||||
}
|
||||
|
||||
fn gg_animation_frame_fn(mut g Context) {
|
||||
g.frame++
|
||||
g.context.clearRect(JS.Number(0), JS.Number(0), JS.Number(g.config.width),
|
||||
JS.Number(g.config.height))
|
||||
// todo(playXE): handle events
|
||||
|
||||
if !isnil(g.config.frame_fn) {
|
||||
f := g.config.frame_fn
|
||||
f(g.user_data)
|
||||
g.needs_refresh = false
|
||||
}
|
||||
|
||||
g.window.requestAnimationFrame(fn [mut g] (time JS.Number) {
|
||||
gg_animation_frame_fn(mut g)
|
||||
})
|
||||
}
|
||||
|
||||
fn (mut g Context) handle_mouse_event(event JS.MouseEvent, typ DOMEventType) Event {
|
||||
mut e := Event{}
|
||||
|
||||
e.typ = typ
|
||||
e.frame_count = g.frame
|
||||
|
||||
match int(event.button) {
|
||||
0 {
|
||||
e.mouse_button = .left
|
||||
}
|
||||
1 {
|
||||
e.mouse_button = .middle
|
||||
}
|
||||
2 {
|
||||
e.mouse_button = .right
|
||||
}
|
||||
else {
|
||||
e.mouse_button = .invalid
|
||||
}
|
||||
}
|
||||
|
||||
e.mouse_x = f32(event.offsetX)
|
||||
e.mouse_y = f32(event.offsetY)
|
||||
e.mouse_dx = f32(event.movementX)
|
||||
e.mouse_dy = f32(event.movementY)
|
||||
bitplace := int(event.button)
|
||||
g.mbtn_mask |= u8(1 << bitplace)
|
||||
// g.mouse_buttons = MouseButtons(g.mbtn_mask)
|
||||
|
||||
g.mouse_pos_x = int(event.offsetX)
|
||||
g.mouse_pos_y = int(event.offsetY)
|
||||
g.mouse_dx = int(event.movementX)
|
||||
g.mouse_dy = int(event.movementY)
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut g Context) handle_keyboard_event(event JS.KeyboardEvent, typ DOMEventType) Event {
|
||||
mut e := Event{}
|
||||
e.typ = typ
|
||||
e.frame_count = g.frame
|
||||
|
||||
match string(event.code) {
|
||||
'Space' {
|
||||
e.key_code = .space
|
||||
}
|
||||
'Minus' {
|
||||
e.key_code = .minus
|
||||
}
|
||||
'Quote' {
|
||||
e.key_code = .apostrophe
|
||||
}
|
||||
'Comma' {
|
||||
e.key_code = .comma
|
||||
}
|
||||
'Period' {
|
||||
e.key_code = .period
|
||||
}
|
||||
'Digit0' {
|
||||
e.key_code = ._0
|
||||
}
|
||||
'Digit1' {
|
||||
e.key_code = ._1
|
||||
}
|
||||
'Digit2' {
|
||||
e.key_code = ._2
|
||||
}
|
||||
'Digit3' {
|
||||
e.key_code = ._3
|
||||
}
|
||||
'Digit4' {
|
||||
e.key_code = ._4
|
||||
}
|
||||
'Digit5' {
|
||||
e.key_code = ._5
|
||||
}
|
||||
'Digit6' {
|
||||
e.key_code = ._6
|
||||
}
|
||||
'Digit7' {
|
||||
e.key_code = ._7
|
||||
}
|
||||
'Digit8' {
|
||||
e.key_code = ._8
|
||||
}
|
||||
'Digit9' {
|
||||
e.key_code = ._9
|
||||
}
|
||||
'Semicolon' {
|
||||
e.key_code = .semicolon
|
||||
}
|
||||
'Equal' {
|
||||
e.key_code = .equal
|
||||
}
|
||||
'KeyA' {
|
||||
e.key_code = .a
|
||||
}
|
||||
'KeyB' {
|
||||
e.key_code = .b
|
||||
}
|
||||
'KeyC' {
|
||||
e.key_code = .c
|
||||
}
|
||||
'KeyD' {
|
||||
e.key_code = .d
|
||||
}
|
||||
'KeyE' {
|
||||
e.key_code = .e
|
||||
}
|
||||
'KeyF' {
|
||||
e.key_code = .f
|
||||
}
|
||||
'KeyG' {
|
||||
e.key_code = .g
|
||||
}
|
||||
'KeyH' {
|
||||
e.key_code = .h
|
||||
}
|
||||
'KeyI' {
|
||||
e.key_code = .i
|
||||
}
|
||||
'KeyJ' {
|
||||
e.key_code = .j
|
||||
}
|
||||
'KeyK' {
|
||||
e.key_code = .k
|
||||
}
|
||||
'KeyL' {
|
||||
e.key_code = .l
|
||||
}
|
||||
'KeyM' {
|
||||
e.key_code = .m
|
||||
}
|
||||
'KeyN' {
|
||||
e.key_code = .n
|
||||
}
|
||||
'KeyO' {
|
||||
e.key_code = .o
|
||||
}
|
||||
'KeyP' {
|
||||
e.key_code = .p
|
||||
}
|
||||
'KeyQ' {
|
||||
e.key_code = .q
|
||||
}
|
||||
'KeyR' {
|
||||
e.key_code = .r
|
||||
}
|
||||
'KeyS' {
|
||||
e.key_code = .s
|
||||
}
|
||||
'KeyT' {
|
||||
e.key_code = .t
|
||||
}
|
||||
'KeyU' {
|
||||
e.key_code = .u
|
||||
}
|
||||
'KeyV' {
|
||||
e.key_code = .v
|
||||
}
|
||||
'KeyW' {
|
||||
e.key_code = .w
|
||||
}
|
||||
'KeyX' {
|
||||
e.key_code = .x
|
||||
}
|
||||
'KeyY' {
|
||||
e.key_code = .y
|
||||
}
|
||||
'KeyZ' {
|
||||
e.key_code = .z
|
||||
}
|
||||
'BracketLeft' {
|
||||
e.key_code = .left_bracket
|
||||
}
|
||||
'BracketRight' {
|
||||
e.key_code = .right_bracket
|
||||
}
|
||||
'Backslash' {
|
||||
e.key_code = .backslash
|
||||
}
|
||||
'Backquote' {
|
||||
e.key_code = .grave_accent
|
||||
}
|
||||
'Escape' {
|
||||
e.key_code = .escape
|
||||
}
|
||||
'Enter' {
|
||||
e.key_code = .enter
|
||||
}
|
||||
'Tab' {
|
||||
e.key_code = .tab
|
||||
}
|
||||
'Backspace' {
|
||||
e.key_code = .backspace
|
||||
}
|
||||
'Insert' {
|
||||
e.key_code = .insert
|
||||
}
|
||||
'Delete' {
|
||||
e.key_code = .delete
|
||||
}
|
||||
'ArrowRight' {
|
||||
e.key_code = .right
|
||||
}
|
||||
'ArrowLeft' {
|
||||
e.key_code = .left
|
||||
}
|
||||
'ArrowUp' {
|
||||
e.key_code = .up
|
||||
}
|
||||
'ArrowDown' {
|
||||
e.key_code = .down
|
||||
}
|
||||
'PageUp' {
|
||||
e.key_code = .page_up
|
||||
}
|
||||
'PageDown' {
|
||||
e.key_code = .page_down
|
||||
}
|
||||
'Home' {
|
||||
e.key_code = .home
|
||||
}
|
||||
'End' {
|
||||
e.key_code = .end
|
||||
}
|
||||
'CapsLock' {
|
||||
e.key_code = .caps_lock
|
||||
}
|
||||
'ScrollLock' {
|
||||
e.key_code = .scroll_lock
|
||||
}
|
||||
'NumLock' {
|
||||
e.key_code = .num_lock
|
||||
}
|
||||
'PrintScreen' {
|
||||
e.key_code = .print_screen
|
||||
}
|
||||
'Pause' {
|
||||
e.key_code = .pause
|
||||
}
|
||||
'ShiftLeft' {
|
||||
e.key_code = .left_shift
|
||||
}
|
||||
'ShiftRight' {
|
||||
e.key_code = .right_shift
|
||||
}
|
||||
'AltLeft' {
|
||||
e.key_code = .left_alt
|
||||
}
|
||||
'AltRight' {
|
||||
e.key_code = .right_alt
|
||||
}
|
||||
'ControlLeft' {
|
||||
e.key_code = .left_control
|
||||
}
|
||||
'ControlRight' {
|
||||
e.key_code = .right_control
|
||||
}
|
||||
else {
|
||||
panic('todo: more keycodes (${string(event.code)})')
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
|
||||
module gg
|
||||
|
||||
pub type FNCb = fn (data voidptr)
|
||||
|
||||
// FNEvent defines the type of a function that will be called for every event
|
||||
pub type FNEvent = fn (e &Event, data voidptr)
|
||||
|
||||
// FNEvent2 same as FNEvent with inverted arguments TODO: deprecate this, in favor of event_fn
|
||||
pub type FNEvent2 = fn (data voidptr, e &Event)
|
||||
|
||||
// FNFail defines the type of a function that will be called when there is a fail
|
||||
pub type FNFail = fn (msg string, data voidptr)
|
||||
|
||||
// FNKeyDown defines the type of a function that will be called for every key pressed down
|
||||
pub type FNKeyDown = fn (c KeyCode, m Modifier, data voidptr)
|
||||
|
||||
// FNKeyUp defines the type of a function that will be called for every release of a pressed key
|
||||
pub type FNKeyUp = fn (c KeyCode, m Modifier, data voidptr)
|
||||
|
||||
// FNMove defines the type of a function that will be called for every mouse move on the screen
|
||||
pub type FNMove = fn (x f32, y f32, data voidptr)
|
||||
|
||||
// FNClick defines the type of a function that will be called for every mouse click
|
||||
pub type FNClick = fn (x f32, y f32, button MouseButton, data voidptr)
|
||||
|
||||
// FNUnClick defines the type of a function that will be called every time a mouse button is released
|
||||
pub type FNUnClick = fn (x f32, y f32, button MouseButton, data voidptr)
|
||||
|
||||
// FNChar defines the type of a function that will be called once per character
|
||||
pub type FNChar = fn (c u32, data voidptr)
|
||||
|
||||
// FNUpdate defines the type of a function, that will be called at the start of each frame
|
||||
// with an argument `dt`, that has the passed time in seconds, since the previous update.
|
||||
pub type FNUpdate = fn (dt f32, data voidptr)
|
||||
|
||||
pub struct PenConfig {
|
||||
pub:
|
||||
color Color
|
||||
line_type PenLineType = .solid
|
||||
thickness f32 = 1
|
||||
}
|
||||
|
||||
@[markused]
|
||||
pub struct Size {
|
||||
pub mut:
|
||||
width int
|
||||
height int
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
module gg
|
||||
|
||||
import os
|
||||
import sokol.sapp
|
||||
|
||||
#include <android/configuration.h>
|
||||
|
||||
fn C.AConfiguration_new() voidptr
|
||||
fn C.AConfiguration_fromAssetManager(voidptr, voidptr)
|
||||
fn C.AConfiguration_getDensity(voidptr) u32
|
||||
fn C.AConfiguration_delete(voidptr)
|
||||
|
||||
// android_dpi_scale returns the scale factor of the device.
|
||||
pub fn android_dpi_scale() f32 {
|
||||
config := C.AConfiguration_new()
|
||||
activity := &os.NativeActivity(sapp.android_get_native_activity())
|
||||
C.AConfiguration_fromAssetManager(config, activity.assetManager)
|
||||
density := C.AConfiguration_getDensity(config)
|
||||
C.AConfiguration_delete(config)
|
||||
return f32(density) / 160
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
module gg
|
||||
|
||||
#include "@VEXEROOT/vlib/gg/gg_darwin.m"
|
||||
|
||||
fn C.gg_get_screen_size() Size
|
||||
|
||||
fn C.darwin_draw_string(x i32, y i32, s string, cfg voidptr)
|
||||
|
||||
fn C.darwin_text_width(s string) i32
|
||||
|
||||
fn C.darwin_text_width_runes(r []rune) i32
|
||||
|
||||
fn C.darwin_window_refresh()
|
||||
|
||||
fn C.darwin_draw_rect(f32, f32, f32, f32, voidptr)
|
||||
|
||||
fn C.darwin_create_image(path string) Image
|
||||
|
||||
fn C.darwin_draw_image(f32, f32, f32, f32, &Image)
|
||||
|
||||
fn C.darwin_draw_circle(f32, f32, f32, voidptr)
|
||||
fn C.darwin_draw_circle_empty(f32, f32, f32, voidptr)
|
||||
@@ -0,0 +1,252 @@
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
NSColor* nscolor(gg__Color c) {
|
||||
float red = (float)c.r / 255.0f;
|
||||
float green = (float)c.g / 255.0f;
|
||||
float blue = (float)c.b / 255.0f;
|
||||
return [NSColor colorWithDeviceRed:red green:green blue:blue alpha:1.0f];
|
||||
}
|
||||
|
||||
NSString* nsstring(string s) {
|
||||
return [[NSString alloc] initWithBytesNoCopy:s.str
|
||||
length:s.len
|
||||
encoding:NSUTF8StringEncoding
|
||||
freeWhenDone:false];
|
||||
}
|
||||
|
||||
gg__Size gg_get_screen_size() {
|
||||
NSScreen *currentScreen = nil;
|
||||
NSWindow *mainWindow = [NSApp mainWindow];
|
||||
// 1. Try screen containing the main window
|
||||
if (mainWindow) {
|
||||
currentScreen = [mainWindow screen];
|
||||
}
|
||||
// 2. If no main window, try the key window (might be different, e.g., a panel)
|
||||
if (!currentScreen) {
|
||||
NSWindow *keyWindow = [NSApp keyWindow];
|
||||
if (keyWindow) {
|
||||
currentScreen = [keyWindow screen];
|
||||
}
|
||||
}
|
||||
// 3. If no relevant window, find the screen containing the mouse cursor
|
||||
if (!currentScreen) {
|
||||
// Get mouse location in global screen coordinates (bottom-left origin)
|
||||
NSPoint mouseLocation = [NSEvent mouseLocation];
|
||||
NSArray<NSScreen *> *screens = [NSScreen screens];
|
||||
for (NSScreen *screen in screens) {
|
||||
// Check if the mouse location is within the screen's frame
|
||||
// Note: Both mouseLocation and screen.frame use bottom-left origin coordinates
|
||||
if (NSMouseInRect(mouseLocation, [screen frame], NO)) {
|
||||
currentScreen = screen;
|
||||
break; // Found the screen with the mouse
|
||||
}
|
||||
}
|
||||
}
|
||||
// 4. As a last resort, fall back to the main screen
|
||||
if (!currentScreen) {
|
||||
NSLog(@"Warning: Could not determine current screen based on window or mouse. Falling back to mainScreen.");
|
||||
currentScreen = [NSScreen mainScreen];
|
||||
}
|
||||
// Now get the size of the determined screen
|
||||
NSDictionary *description = [currentScreen deviceDescription];
|
||||
// Use NSDeviceSize to get pixel dimensions
|
||||
NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue];
|
||||
// Create the V gg.Size object
|
||||
gg__Size res;
|
||||
res.width = displayPixelSize.width;
|
||||
res.height = displayPixelSize.height;
|
||||
return res;
|
||||
}
|
||||
|
||||
void gg_macos_resize_window(void *window_ptr, int width, int height) {
|
||||
if (window_ptr == nil || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)window_ptr;
|
||||
[window setContentSize:NSMakeSize((CGFloat)width, (CGFloat)height)];
|
||||
}
|
||||
|
||||
void gg_macos_set_window_resizable(void *window_ptr, bool resizable) {
|
||||
if (window_ptr == nil) {
|
||||
return;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)window_ptr;
|
||||
NSWindowStyleMask style = [window styleMask];
|
||||
if (resizable) {
|
||||
style |= NSWindowStyleMaskResizable;
|
||||
} else {
|
||||
style &= ~NSWindowStyleMaskResizable;
|
||||
}
|
||||
[window setStyleMask:style];
|
||||
}
|
||||
|
||||
// When non-zero, every string is drawn in a monospace font regardless of the
|
||||
// per-call cfg.mono flag. Lets an app switch the whole UI to a fixed-width font
|
||||
// so text widths can be computed as char_count * advance (no CoreText layout).
|
||||
// Default 0 => behaviour unchanged for every other gg app.
|
||||
int g_gg_force_mono = 0;
|
||||
void gg_set_force_mono(int on) { g_gg_force_mono = on; }
|
||||
|
||||
// Cache of the (font + color) attribute dictionaries passed to -drawAtPoint:.
|
||||
// darwin_draw_string is called once per text run, every frame; building a fresh
|
||||
// NSFont (font lookup), NSColor, and NSDictionary on each call was pure ObjC
|
||||
// churn (CPU + autorelease-pool pressure). The set of distinct (size, style,
|
||||
// color) combinations a UI actually uses is tiny (a few sizes × a theme palette),
|
||||
// so we memoize the finished attribute dict. Keyed on everything that changes the
|
||||
// dict — color rgb (nscolor ignores alpha), size, bold, mono, and the global
|
||||
// force-mono flag. The dictionary retains each cached attr dict (ARC), so they
|
||||
// persist for the process; bounded by the number of distinct combinations.
|
||||
static NSMutableDictionary<NSNumber*, NSDictionary*>* g_text_attr_cache = nil;
|
||||
|
||||
static NSDictionary* darwin_text_attrs(gg__TextCfg cfg) {
|
||||
uint64_t key = ((uint64_t)cfg.color.r)
|
||||
| ((uint64_t)cfg.color.g << 8)
|
||||
| ((uint64_t)cfg.color.b << 16)
|
||||
| (((uint64_t)cfg.size & 0xFFFF) << 24)
|
||||
| ((uint64_t)(cfg.bold ? 1 : 0) << 40)
|
||||
| ((uint64_t)(cfg.mono ? 1 : 0) << 41)
|
||||
| ((uint64_t)(g_gg_force_mono ? 1 : 0) << 42);
|
||||
if (g_text_attr_cache == nil) {
|
||||
g_text_attr_cache = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
NSNumber* k = @(key);
|
||||
NSDictionary* cached = [g_text_attr_cache objectForKey:k];
|
||||
if (cached != nil) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
NSFont* font = [NSFont userFontOfSize:cfg.size];
|
||||
// # NSFont* font = [NSFont fontWithName:@"Roboto Mono" size:cfg.size];
|
||||
if (g_gg_force_mono) {
|
||||
// Global monospace mode: requested size minus 1, kept in sync with the
|
||||
// app-side width metric (see fuse_text_width / mono_char_advance).
|
||||
font = [NSFont fontWithName:@"Menlo" size:cfg.size - 1];
|
||||
} else if (cfg.mono) {
|
||||
// # font = [NSFont fontWithName:@"Roboto Mono" size:cfg.size];
|
||||
font = [NSFont fontWithName:@"Menlo" size:cfg.size - 5];
|
||||
}
|
||||
if (cfg.bold) {
|
||||
font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask];
|
||||
}
|
||||
|
||||
NSDictionary* attr = @{
|
||||
NSForegroundColorAttributeName : nscolor(cfg.color),
|
||||
// NSParagraphStyleAttributeName: paragraphStyle,
|
||||
NSFontAttributeName : font,
|
||||
};
|
||||
[g_text_attr_cache setObject:attr forKey:k];
|
||||
return attr;
|
||||
}
|
||||
|
||||
void darwin_draw_string(int x, int y, string s, gg__TextCfg cfg) {
|
||||
NSDictionary* attr = darwin_text_attrs(cfg);
|
||||
[nsstring(s) drawAtPoint:NSMakePoint(x, y - 15) withAttributes:attr];
|
||||
}
|
||||
|
||||
int darwin_text_width(string s) {
|
||||
// println('text_width "${s}" len=${s.len}')
|
||||
NSString* n = @"";
|
||||
if (s.len == 1) {
|
||||
// println('len=1')
|
||||
n = [NSString stringWithFormat:@"%c", s.str[0]];
|
||||
} else {
|
||||
n = nsstring(s);
|
||||
}
|
||||
/*
|
||||
# if (!defaultFont){
|
||||
# defaultFont = [NSFont userFontOfSize: ui__DEFAULT_FONT_SIZE];
|
||||
# }
|
||||
# NSDictionary *attrs = @{
|
||||
# NSFontAttributeName: defaultFont,
|
||||
# };
|
||||
*/
|
||||
NSSize size = [n sizeWithAttributes:nil];
|
||||
// # printf("!!!%f\n", ceil(size.width));
|
||||
return (int)(ceil(size.width));
|
||||
}
|
||||
|
||||
void darwin_draw_rect(float x, float y, float width, float height, gg__Color c) {
|
||||
NSColor* color = nscolor(c);
|
||||
NSRect rect = NSMakeRect(x, y, width, height);
|
||||
[color setFill];
|
||||
NSRectFill(rect);
|
||||
}
|
||||
|
||||
static void mark_view_tree_needs_display(NSView *view) {
|
||||
if (view == nil) {
|
||||
return;
|
||||
}
|
||||
[view setNeedsDisplay:YES];
|
||||
for (NSView *subview in [view subviews]) {
|
||||
mark_view_tree_needs_display(subview);
|
||||
}
|
||||
}
|
||||
|
||||
void darwin_window_refresh() {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSWindow *window = [NSApp mainWindow];
|
||||
if (window == nil) {
|
||||
window = [NSApp keyWindow];
|
||||
}
|
||||
if (window == nil) {
|
||||
return;
|
||||
}
|
||||
NSView *contentView = [window contentView];
|
||||
if (contentView == nil) {
|
||||
return;
|
||||
}
|
||||
mark_view_tree_needs_display(contentView);
|
||||
[window displayIfNeeded];
|
||||
});
|
||||
|
||||
// puts("refresh");
|
||||
//[[NSApp mainWindow].contentView drawRect:NSMakeRect(0,0,2000,2000)];
|
||||
//[[NSGraphicsContext currentContext] flushGraphics];
|
||||
}
|
||||
|
||||
gg__Image darwin_create_image(string path_) {
|
||||
// file = file.trim_space()
|
||||
NSString* path = nsstring(path_);
|
||||
NSImage* img = [[NSImage alloc] initWithContentsOfFile:path];
|
||||
if (img == 0) {
|
||||
}
|
||||
NSSize size = [img size];
|
||||
gg__Image res;
|
||||
res.width = size.width;
|
||||
res.height = size.height;
|
||||
res.path = path_;
|
||||
res.ok = true;
|
||||
// printf("inited img width=%d\n", res.width) ;
|
||||
// need __bridge_retained so that the pointer is not freed by ARC
|
||||
res.data = (__bridge_retained voidptr)(img);
|
||||
return res;
|
||||
}
|
||||
|
||||
void darwin_draw_image(float x, float y, float w, float h, gg__Image* img) {
|
||||
NSImage* i = (__bridge NSImage*)(img->data);
|
||||
[i drawInRect:NSMakeRect(x, y, w, h)];
|
||||
}
|
||||
|
||||
void darwin_draw_circle(float x, float y, float d, gg__Color color) {
|
||||
NSColor* c = nscolor(color);
|
||||
NSRect rect = NSMakeRect(x, y, d * 2, d * 2);
|
||||
NSBezierPath* circlePath = [NSBezierPath bezierPath];
|
||||
[circlePath appendBezierPathWithOvalInRect:rect];
|
||||
[c setFill];
|
||||
// [circlePath stroke];
|
||||
[circlePath fill];
|
||||
// NSRectFill(rect);
|
||||
}
|
||||
|
||||
void darwin_draw_circle_empty(float x, float y, float d, gg__Color color) {
|
||||
NSColor* outlineColor = nscolor(color);
|
||||
CGFloat outlineWidth = 1.0; //2.0;
|
||||
|
||||
NSRect rect = NSMakeRect(x, y, d * 2, d * 2);
|
||||
NSBezierPath* circlePath = [NSBezierPath bezierPath];
|
||||
[circlePath appendBezierPathWithOvalInRect:rect];
|
||||
|
||||
[outlineColor setStroke];
|
||||
[circlePath setLineWidth:outlineWidth];
|
||||
[circlePath stroke];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
module gg
|
||||
|
||||
import sokol.sgl
|
||||
|
||||
// Stuff for ui from now for screenshot (that would be interesting for gg if screenshot is implemented also for gg)
|
||||
|
||||
// required for ui.DrawDevice interface (with &gg.Context as an instance)
|
||||
pub fn (ctx &Context) scissor_rect(x int, y int, w int, h int) {
|
||||
sgl.scissor_rect(int(ctx.scale * x), int(ctx.scale * y), int(ctx.scale * w),
|
||||
int(ctx.scale * h), true)
|
||||
}
|
||||
|
||||
// empty function
|
||||
pub fn (ctx &Context) has_text_style() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// empty function
|
||||
pub fn (ctx &Context) set_text_style(font_name string, font_path string, size int, color Color, align int,
|
||||
vertical_align int) {
|
||||
}
|
||||
|
||||
// default draw_text (draw_text_def but without set_text_cfg)
|
||||
pub fn (ctx &Context) draw_text_default(x int, y int, text string) {
|
||||
scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
|
||||
ctx.ft.fons.draw_text(x * scale, y * scale, text) // TODO: check offsets/alignment
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module gg
|
||||
|
||||
import os
|
||||
import stbi
|
||||
import sokol.gfx
|
||||
import sokol.sgl
|
||||
|
||||
// Image holds the fields and data needed to
|
||||
// represent a bitmap/pixel based image in memory.
|
||||
@[heap; markused]
|
||||
pub struct Image {
|
||||
pub mut:
|
||||
id int
|
||||
width int
|
||||
height int
|
||||
nr_channels int
|
||||
nr_mipmaps int
|
||||
ok bool
|
||||
data voidptr
|
||||
ext string
|
||||
simg_ok bool
|
||||
simg gfx.Image
|
||||
ssmp gfx.Sampler
|
||||
path string
|
||||
texture_filter TextureFilter = .linear
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ImageConfig {
|
||||
pub:
|
||||
texture_filter ?TextureFilter
|
||||
max_mipmaps int = 1 // a value below 1 means as many as possible
|
||||
}
|
||||
|
||||
// destroy GPU resources associated with the image
|
||||
fn (image &Image) destroy() {
|
||||
if image.ok {
|
||||
if image.simg.id > 0 {
|
||||
gfx.destroy_image(image.simg)
|
||||
}
|
||||
if image.ssmp.id > 0 {
|
||||
gfx.destroy_sampler(image.ssmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create_image_with_filter creates an `Image` from `file` with the requested texture filter.
|
||||
@[deprecated: 'use Context.create_image instead']
|
||||
pub fn (mut ctx Context) create_image_with_filter(file string, texture_filter TextureFilter) !Image {
|
||||
return ctx.create_image(file, texture_filter: texture_filter)!
|
||||
}
|
||||
|
||||
// create_image creates an `Image` from `file`.
|
||||
pub fn (mut ctx Context) create_image(file string, cfg ImageConfig) !Image {
|
||||
if !os.exists(file) {
|
||||
$if android {
|
||||
image_data := os.read_apk_asset(file)!
|
||||
mut image := ctx.create_image_from_byte_array(image_data, cfg)!
|
||||
|
||||
image.path = file
|
||||
|
||||
return image
|
||||
} $else {
|
||||
return error('image file "${file}" not found')
|
||||
}
|
||||
}
|
||||
$if macos {
|
||||
if ctx.native_rendering {
|
||||
mut img := C.darwin_create_image(file)
|
||||
img.texture_filter = cfg.texture_filter or { ctx.config.texture_filter }
|
||||
|
||||
img.id = ctx.image_cache.len
|
||||
unsafe {
|
||||
ctx.image_cache << img
|
||||
}
|
||||
return img
|
||||
}
|
||||
}
|
||||
|
||||
mut img := load_image(file)!
|
||||
img.id = ctx.image_cache.len
|
||||
img.texture_filter = cfg.texture_filter or { ctx.config.texture_filter }
|
||||
unsafe {
|
||||
ctx.image_cache << img
|
||||
}
|
||||
if gfx.is_valid() {
|
||||
img.init_sokol_image()
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// adapted from https://github.com/floooh/sokol/issues/102#issuecomment-2926566603
|
||||
fn sokol_mipmap(mut simg_desc gfx.ImageDesc, max_mipmaps int) {
|
||||
mut levels := 0
|
||||
mut size := 0
|
||||
width := simg_desc.width
|
||||
height := simg_desc.height
|
||||
mipmaps := if max_mipmaps < gfx.sg_max_mipmaps && max_mipmaps > 0 {
|
||||
max_mipmaps
|
||||
} else {
|
||||
gfx.sg_max_mipmaps
|
||||
}
|
||||
for i in 1 .. mipmaps {
|
||||
w := width >> i
|
||||
h := height >> i
|
||||
if w < 1 || h < 1 { break
|
||||
}
|
||||
size += w * h * 4 // 4 = img.nr_channels
|
||||
levels++
|
||||
}
|
||||
// use unsafe to access the data because we have to anyway
|
||||
mut target := unsafe { malloc(size) }
|
||||
mut src_width := width
|
||||
mut src_height := height
|
||||
for level in 1 .. levels {
|
||||
src := &u8(simg_desc.data.subimage[0][level - 1].ptr)
|
||||
target_width := src_width / 2
|
||||
target_height := src_height / 2
|
||||
for x in 0 .. target_width {
|
||||
for y in 0 .. target_height {
|
||||
// channels RGBA
|
||||
chans := 4
|
||||
for ch in 0 .. 4 {
|
||||
// avererage colors in a 2x2 square over the source image where (x*2|y*2) is the lower left corner
|
||||
mut color := 0
|
||||
sx := x * 2
|
||||
sy := y * 2
|
||||
color += unsafe { src[(sx + src_width * sy) * chans + ch] }
|
||||
color += unsafe { src[(sx + src_width * (sy + 1)) * chans + ch] }
|
||||
color += unsafe { src[((sx + 1) + src_width * (sy + 1)) * chans + ch] }
|
||||
color += unsafe { src[((sx + 1) + src_width * sy) * chans + ch] }
|
||||
color /= 4
|
||||
unsafe {
|
||||
target[(x + y * target_width) * chans + ch] = u8(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
src_width = target_width
|
||||
src_height = target_height
|
||||
simg_desc.data.subimage[0][level].ptr = target
|
||||
simg_desc.data.subimage[0][level].size = usize(target_width * target_height * 4)
|
||||
unsafe {
|
||||
target += target_width * target_height * 4
|
||||
}
|
||||
}
|
||||
simg_desc.num_mipmaps = levels
|
||||
}
|
||||
|
||||
// init_sokol_image initializes this `Image` for use with the
|
||||
// sokol graphical backend system.
|
||||
pub fn (mut img Image) init_sokol_image() &Image {
|
||||
// println('\n init sokol image ${img.path} ok=${img.simg_ok}')
|
||||
mut img_desc := gfx.ImageDesc{
|
||||
width: img.width
|
||||
height: img.height
|
||||
// wrap_u: .clamp_to_edge // XTODO SAMPLER
|
||||
// wrap_v: .clamp_to_edge
|
||||
label: &char(img.path.str)
|
||||
d3d11_texture: 0
|
||||
}
|
||||
|
||||
// NOTE the following code, sometimes, result in hard-to-detect visual errors/bugs:
|
||||
// img_size := usize(img.nr_channels * img.width * img.height)
|
||||
// As an example see https://github.com/vlang/vab/issues/239
|
||||
// The image will come out blank for some reason and no SOKOL_ASSERT
|
||||
// nor any CI check will/can currently catch this.
|
||||
// Since all of gg currently runs with more or less *defaults* from sokol_gfx/sokol_gl
|
||||
// we should currently just use the sum of each of the RGB and A channels (= 4) here instead.
|
||||
// Optimized PNG images that have no alpha channel is often optimized to only have
|
||||
// 3 (or less) channels which stbi will correctly detect and set as `img.nr_channels`
|
||||
// but the current sokol_gl context setup expects 4. It *should* be the same with
|
||||
// all other stbi supported formats.
|
||||
img_size := usize(4 * img.width * img.height)
|
||||
img_desc.data.subimage[0][0] = gfx.Range{
|
||||
ptr: img.data
|
||||
size: img_size
|
||||
}
|
||||
sokol_mipmap(mut img_desc, img.nr_mipmaps)
|
||||
img.nr_mipmaps = img_desc.num_mipmaps
|
||||
img.simg = gfx.make_image(&img_desc)
|
||||
gfx_filter := img.texture_filter.gfx_filter()
|
||||
|
||||
mut smp_desc := gfx.SamplerDesc{
|
||||
min_filter: gfx_filter
|
||||
mag_filter: gfx_filter
|
||||
mipmap_filter: .linear
|
||||
wrap_u: .clamp_to_edge
|
||||
wrap_v: .clamp_to_edge
|
||||
}
|
||||
|
||||
img.ssmp = gfx.make_sampler(&smp_desc)
|
||||
|
||||
img.simg_ok = true
|
||||
img.ok = true
|
||||
return img
|
||||
}
|
||||
|
||||
// draw_image draws the provided image onto the screen.
|
||||
pub fn (ctx &Context) draw_image(x f32, y f32, width f32, height f32, img_ &Image) {
|
||||
ctx.draw_image_with_config(
|
||||
img: img_
|
||||
img_rect: Rect{x, y, width, height}
|
||||
part_rect: Rect{0, 0, img_.width, img_.height}
|
||||
)
|
||||
}
|
||||
|
||||
// new_streaming_image returns a cached `image_idx` of a special image, that
|
||||
// can be updated *each frame* by calling: gg.update_pixel_data(image_idx, buf)
|
||||
// ... where buf is a pointer to the actual pixel data for the image.
|
||||
// Note: you still need to call app.gg.draw_image after that, to actually draw it.
|
||||
// Note: Sokol needs to be setup, *before* calling this function. In practice,
|
||||
// this often means, that you have to call it once in the `init_fn` callback of
|
||||
// gg.new_context, or gg.start, and then store the result in your app instance.
|
||||
pub fn (mut ctx Context) new_streaming_image(w int, h int, channels int, sicfg StreamingImageConfig) int {
|
||||
mut img := Image{}
|
||||
img.width = w
|
||||
img.height = h
|
||||
img.nr_channels = channels // 4 bytes per pixel for .rgba8, see pixel_format
|
||||
mut img_desc := gfx.ImageDesc{
|
||||
width: img.width
|
||||
height: img.height
|
||||
pixel_format: sicfg.pixel_format
|
||||
num_slices: 1
|
||||
num_mipmaps: 1
|
||||
usage: .stream
|
||||
label: &char(img.path.str)
|
||||
}
|
||||
// Sokol requires that streamed images have NO .ptr/.size initially:
|
||||
img_desc.data.subimage[0][0] = gfx.Range{
|
||||
ptr: 0
|
||||
size: usize(0)
|
||||
}
|
||||
img.simg = gfx.make_image(&img_desc)
|
||||
|
||||
mut smp_desc := gfx.SamplerDesc{
|
||||
wrap_u: sicfg.wrap_u // SAMPLER
|
||||
wrap_v: sicfg.wrap_v
|
||||
min_filter: sicfg.min_filter
|
||||
mag_filter: sicfg.mag_filter
|
||||
}
|
||||
|
||||
img.ssmp = gfx.make_sampler(&smp_desc)
|
||||
img.simg_ok = true
|
||||
img.ok = true
|
||||
img_idx := ctx.cache_image(img)
|
||||
return img_idx
|
||||
}
|
||||
|
||||
// update_pixel_data is a helper for working with image streams (i.e. images,
|
||||
// that are updated dynamically by the CPU on each frame)
|
||||
pub fn (mut ctx Context) update_pixel_data(cached_image_idx int, buf &u8) {
|
||||
mut image := ctx.get_cached_image_by_idx(cached_image_idx)
|
||||
image.update_pixel_data(buf)
|
||||
}
|
||||
|
||||
// update_pixel_data updates the sokol specific pixel data associated
|
||||
// with this `Image`.
|
||||
pub fn (mut img Image) update_pixel_data(buf &u8) {
|
||||
mut data := gfx.ImageData{}
|
||||
data.subimage[0][0].ptr = buf
|
||||
data.subimage[0][0].size = usize(img.width * img.height * img.nr_channels)
|
||||
gfx.update_image(img.simg, &data)
|
||||
}
|
||||
|
||||
// create_image_with_size creates an `Image` from `file` in the given
|
||||
// `width` x `height` dimension.
|
||||
//
|
||||
// TODO: copypasta
|
||||
@[deprecated]
|
||||
pub fn (mut ctx Context) create_image_with_size(file string, width int, height int) Image {
|
||||
if !gfx.is_valid() {
|
||||
// Sokol is not initialized yet, add stbi object to a queue/cache
|
||||
// ctx.image_queue << file
|
||||
mut img := load_image(file) or { return Image{} }
|
||||
img.texture_filter = ctx.config.texture_filter
|
||||
img.width = width
|
||||
img.height = height
|
||||
img.ok = false
|
||||
img.id = ctx.image_cache.len
|
||||
ctx.image_cache << img
|
||||
return img
|
||||
}
|
||||
return ctx.create_image(file) or { Image{} }
|
||||
}
|
||||
|
||||
fn load_image(file string) !Image {
|
||||
if !os.exists(file) {
|
||||
return error('image file "${file}" not found')
|
||||
}
|
||||
stb_img := stbi.load(file)!
|
||||
return Image{
|
||||
width: stb_img.width
|
||||
height: stb_img.height
|
||||
nr_channels: stb_img.nr_channels
|
||||
ok: stb_img.ok
|
||||
data: stb_img.data
|
||||
ext: stb_img.ext
|
||||
path: file
|
||||
}
|
||||
}
|
||||
|
||||
// create_image_from_memory creates an `Image` from the
|
||||
// memory buffer `buf` of size `bufsize`.
|
||||
//
|
||||
// See also: create_image_from_byte_array
|
||||
pub fn (mut ctx Context) create_image_from_memory(buf &u8, bufsize int, cfg ImageConfig) !Image {
|
||||
stb_img := stbi.load_from_memory(buf, bufsize)!
|
||||
mut img := Image{
|
||||
width: stb_img.width
|
||||
height: stb_img.height
|
||||
nr_channels: stb_img.nr_channels
|
||||
ok: stb_img.ok
|
||||
data: stb_img.data
|
||||
ext: stb_img.ext
|
||||
id: ctx.image_cache.len
|
||||
texture_filter: cfg.texture_filter or { ctx.config.texture_filter }
|
||||
}
|
||||
if gfx.is_valid() {
|
||||
img.init_sokol_image()
|
||||
}
|
||||
ctx.image_cache << img
|
||||
return img
|
||||
}
|
||||
|
||||
// create_image_from_memory_with_filter creates an `Image` from `buf` with the requested texture filter.
|
||||
@[deprecated: 'use Context.create_image_from_memory instead']
|
||||
pub fn (mut ctx Context) create_image_from_memory_with_filter(buf &u8, bufsize int, texture_filter TextureFilter) !Image {
|
||||
return ctx.create_image_from_memory(buf, bufsize, texture_filter: texture_filter)
|
||||
}
|
||||
|
||||
// create_image_from_byte_array creates an `Image` from the
|
||||
// byte array `b`.
|
||||
//
|
||||
// See also: create_image_from_memory
|
||||
pub fn (mut ctx Context) create_image_from_byte_array(b []u8, cfg ImageConfig) !Image {
|
||||
return ctx.create_image_from_memory(b.data, b.len, cfg)
|
||||
}
|
||||
|
||||
// create_image_from_byte_array_with_filter creates an `Image` from `b` with the requested texture filter.
|
||||
@[deprecated: 'use Context.create_image_from_byte_array instead']
|
||||
pub fn (mut ctx Context) create_image_from_byte_array_with_filter(b []u8, texture_filter TextureFilter) !Image {
|
||||
return ctx.create_image_from_memory(b.data, b.len, texture_filter: texture_filter)
|
||||
}
|
||||
|
||||
pub struct StreamingImageConfig {
|
||||
pub:
|
||||
pixel_format gfx.PixelFormat = .rgba8
|
||||
wrap_u gfx.Wrap = .clamp_to_edge
|
||||
wrap_v gfx.Wrap = .clamp_to_edge
|
||||
min_filter gfx.Filter = .linear
|
||||
mag_filter gfx.Filter = .linear
|
||||
num_mipmaps int = 1
|
||||
num_slices int = 1
|
||||
}
|
||||
|
||||
fn (filter TextureFilter) gfx_filter() gfx.Filter {
|
||||
return match filter {
|
||||
.linear { .linear }
|
||||
.nearest { .nearest }
|
||||
}
|
||||
}
|
||||
|
||||
// draw_image_with_config takes in a config that details how the
|
||||
// provided image should be drawn onto the screen
|
||||
pub fn (ctx &Context) draw_image_with_config(config DrawImageConfig) {
|
||||
$if macos {
|
||||
if ctx.native_rendering {
|
||||
unsafe {
|
||||
mut img := config.img
|
||||
if config.img == nil {
|
||||
// Get image by id
|
||||
if config.img_id > 0 {
|
||||
img = &ctx.image_cache[config.img_id]
|
||||
} else {
|
||||
$if !noggverbose ? {
|
||||
eprintln('gg: failed to get image to draw natively')
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if img.id >= ctx.image_cache.len {
|
||||
eprintln('gg: draw_image() bad img id ${img.id} (img cache len = ${ctx.image_cache.len})')
|
||||
return
|
||||
}
|
||||
if img.width == 0 {
|
||||
println('gg: draw_image() width=0')
|
||||
return
|
||||
}
|
||||
if !os.exists(img.path) {
|
||||
println('not exist path')
|
||||
return
|
||||
}
|
||||
x := config.img_rect.x
|
||||
y := config.img_rect.y
|
||||
width := if config.img_rect.width == 0 {
|
||||
// Calculate the width by dividing it by the height ratio.
|
||||
// e.g. the original image is 100x100, we're drawing 0x20. Find the ratio (5)
|
||||
// by dividing the height 100 by 20, and then divide the width by 5.
|
||||
f32(img.width / (img.height / config.img_rect.height))
|
||||
} else {
|
||||
config.img_rect.width
|
||||
}
|
||||
height := if config.img_rect.height == 0 {
|
||||
// Same as above.
|
||||
f32(img.height / (img.width / config.img_rect.width))
|
||||
} else {
|
||||
config.img_rect.height
|
||||
}
|
||||
C.darwin_draw_image(x, ctx.height - (y + config.img_rect.height), width, height,
|
||||
img)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id := if !isnil(config.img) { config.img.id } else { config.img_id }
|
||||
if id >= ctx.image_cache.len {
|
||||
eprintln('gg: draw_image() bad img id ${id} (img cache len = ${ctx.image_cache.len})')
|
||||
return
|
||||
}
|
||||
|
||||
img := &ctx.image_cache[id]
|
||||
if !img.simg_ok {
|
||||
return
|
||||
}
|
||||
|
||||
mut img_rect := config.img_rect
|
||||
if img_rect.width == 0 && img_rect.height == 0 {
|
||||
img_rect = Rect{img_rect.x, img_rect.y, img.width, img.height}
|
||||
}
|
||||
|
||||
mut part_rect := config.part_rect
|
||||
if part_rect.width == 0 && part_rect.height == 0 {
|
||||
part_rect = Rect{part_rect.x, part_rect.y, img.width, img.height}
|
||||
}
|
||||
|
||||
u0 := part_rect.x / img.width
|
||||
v0 := part_rect.y / img.height
|
||||
u1 := (part_rect.x + part_rect.width) / img.width
|
||||
v1 := (part_rect.y + part_rect.height) / img.height
|
||||
x0 := img_rect.x * ctx.scale
|
||||
y0 := img_rect.y * ctx.scale
|
||||
x1 := (img_rect.x + img_rect.width) * ctx.scale
|
||||
mut y1 := (img_rect.y + img_rect.height) * ctx.scale
|
||||
if img_rect.height == 0 {
|
||||
scale := f32(img.width) / f32(img_rect.width)
|
||||
y1 = f32(img_rect.y + int(f32(img.height) / scale)) * ctx.scale
|
||||
}
|
||||
|
||||
flip_x := config.flip_x
|
||||
flip_y := config.flip_y
|
||||
|
||||
mut u0f := if !flip_x { u0 } else { u1 }
|
||||
mut u1f := if !flip_x { u1 } else { u0 }
|
||||
mut v0f := if !flip_y { v0 } else { v1 }
|
||||
mut v1f := if !flip_y { v1 } else { v0 }
|
||||
|
||||
// FIXME: is this okay?
|
||||
match config.effect {
|
||||
.alpha { sgl.load_pipeline(ctx.pipeline.alpha) }
|
||||
.add { sgl.load_pipeline(ctx.pipeline.add) }
|
||||
}
|
||||
|
||||
sgl.enable_texture()
|
||||
sgl.texture(img.simg, img.ssmp)
|
||||
|
||||
if config.rotation != 0 {
|
||||
width := img_rect.width * ctx.scale
|
||||
height := (if img_rect.height > 0 { img_rect.height } else { img.height }) * ctx.scale
|
||||
|
||||
sgl.push_matrix()
|
||||
sgl.translate(x0 + (width / 2), y0 + (height / 2), 0)
|
||||
sgl.rotate(sgl.rad(-config.rotation), 0, 0, 1)
|
||||
sgl.translate(-x0 - (width / 2), -y0 - (height / 2), 0)
|
||||
}
|
||||
|
||||
sgl.begin_quads()
|
||||
sgl.c4b(config.color.r, config.color.g, config.color.b, config.color.a)
|
||||
sgl.v3f_t2f(x0, y0, config.z, u0f, v0f)
|
||||
sgl.v3f_t2f(x1, y0, config.z, u1f, v0f)
|
||||
sgl.v3f_t2f(x1, y1, config.z, u1f, v1f)
|
||||
sgl.v3f_t2f(x0, y1, config.z, u0f, v1f)
|
||||
sgl.end()
|
||||
|
||||
if config.rotation != 0 {
|
||||
sgl.pop_matrix()
|
||||
}
|
||||
|
||||
sgl.disable_texture()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
module gg
|
||||
|
||||
@[heap]
|
||||
pub struct Image {
|
||||
pub mut:
|
||||
id int
|
||||
width int
|
||||
height int
|
||||
nr_channels int
|
||||
ok bool
|
||||
data voidptr
|
||||
ext string
|
||||
|
||||
path string
|
||||
}
|
||||
|
||||
pub fn (ctx &Context) draw_image_with_config(config DrawImageConfig) {
|
||||
}
|
||||
|
||||
// destroy GPU resources associated with the image
|
||||
fn (img &Image) destroy() {
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module gg
|
||||
|
||||
// TextureFilter controls how gg samples textures when images are scaled.
|
||||
pub enum TextureFilter {
|
||||
linear
|
||||
nearest
|
||||
}
|
||||
|
||||
// DrawImageConfig struct defines the various options
|
||||
// that can be used to draw an image onto the screen
|
||||
pub struct DrawImageConfig {
|
||||
pub mut:
|
||||
flip_x bool // set to true, if you need to flip the image horizontally (around a vertical axis), <- will become ->
|
||||
flip_y bool // set to true, if you need to flip the image vertically (around a horizontal axiz), -\/- will become -/\-
|
||||
img &Image = unsafe { nil }
|
||||
img_id int
|
||||
img_rect Rect // defines the size and position on image when rendering to the screen
|
||||
part_rect Rect // defines the size and position of part of the image to use when rendering
|
||||
z f32
|
||||
color Color = white
|
||||
effect ImageEffect = .alpha
|
||||
|
||||
rotation f32 // the amount to rotate the image in degrees, counterclockwise. Use a negative value, to rotate it clockwise.
|
||||
}
|
||||
|
||||
pub enum ImageEffect {
|
||||
// TODO(FireRedz): Add more effects
|
||||
alpha
|
||||
add
|
||||
}
|
||||
|
||||
// Rect represents a rectangular shape in `gg`.
|
||||
pub struct Rect {
|
||||
pub mut:
|
||||
x f32
|
||||
y f32
|
||||
width f32
|
||||
height f32
|
||||
}
|
||||
|
||||
// cache_image caches the image `img` in memory for later reuse.
|
||||
// cache_image returns the cache index of the cached image.
|
||||
//
|
||||
// See also: get_cached_image_by_idx
|
||||
// See also: remove_cached_image_by_idx
|
||||
pub fn (mut ctx Context) cache_image(img Image) int {
|
||||
ctx.image_cache << img
|
||||
image_idx := ctx.image_cache.len - 1
|
||||
ctx.image_cache[image_idx].id = image_idx
|
||||
return image_idx
|
||||
}
|
||||
|
||||
const missing_image = Image{}
|
||||
|
||||
// get_cached_image_by_idx returns a cached `Image` identified by `image_idx`.
|
||||
// If image not found, returns `Image{ok: false}`
|
||||
//
|
||||
// See also: cache_image
|
||||
// See also: remove_cached_image_by_idx
|
||||
pub fn (mut ctx Context) get_cached_image_by_idx(image_idx int) &Image {
|
||||
if image_idx < 0 || image_idx > ctx.image_cache.len - 1 {
|
||||
return unsafe { &missing_image }
|
||||
}
|
||||
return &ctx.image_cache[image_idx]
|
||||
}
|
||||
|
||||
// remove_cached_image_by_idx removes an `Image` identified by `image_idx` from the
|
||||
// image cache.
|
||||
//
|
||||
// See also: cache_image
|
||||
// See also: get_cached_image_by_idx
|
||||
pub fn (mut ctx Context) remove_cached_image_by_idx(image_idx int) {
|
||||
if image_idx < 0 || image_idx > ctx.image_cache.len - 1 {
|
||||
return
|
||||
}
|
||||
ctx.image_cache[image_idx].destroy()
|
||||
ctx.image_cache[image_idx] = unsafe { &missing_image }
|
||||
}
|
||||
|
||||
// Draw part of an image using uv coordinates
|
||||
// img_rect is the size and position (in pixels on screen) of the displayed rectangle (ie the draw_image args)
|
||||
// part_rect is the size and position (in absolute pixels in the image) of the wanted part
|
||||
// eg. On a 600*600 context, to display only the first 400*400 pixels of a 2000*2000 image
|
||||
// on the entire context surface, call :
|
||||
// draw_image_part(Rect{0, 0, 600, 600}, Rect{0, 0, 400, 400}, img)
|
||||
pub fn (ctx &Context) draw_image_part(img_rect Rect, part_rect Rect, img_ &Image) {
|
||||
ctx.draw_image_with_config(
|
||||
img: img_
|
||||
img_rect: img_rect
|
||||
part_rect: part_rect
|
||||
)
|
||||
}
|
||||
|
||||
// draw_image_flipped draws the provided image flipped horizontally (use `draw_image_with_config` to flip vertically)
|
||||
pub fn (ctx &Context) draw_image_flipped(x f32, y f32, width f32, height f32, img_ &Image) {
|
||||
ctx.draw_image_with_config(
|
||||
flip_x: true
|
||||
img: img_
|
||||
img_rect: Rect{x, y, width, height}
|
||||
)
|
||||
}
|
||||
|
||||
// draw_image_by_id draws an image by its id
|
||||
pub fn (ctx &Context) draw_image_by_id(x f32, y f32, width f32, height f32, id int) {
|
||||
ctx.draw_image_with_config(
|
||||
img_id: id
|
||||
img_rect: Rect{x, y, width, height}
|
||||
)
|
||||
}
|
||||
|
||||
// draw_image_3d draws an image with a z depth
|
||||
pub fn (ctx &Context) draw_image_3d(x f32, y f32, z f32, width f32, height f32, img_ &Image) {
|
||||
ctx.draw_image_with_config(
|
||||
img: img_
|
||||
img_rect: Rect{x, y, width, height}
|
||||
z: z
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// vtest build: !docker-ubuntu-musl // needs GL/gl.h
|
||||
module gg
|
||||
|
||||
import os
|
||||
|
||||
const background_path = os.join_path(@VEXEROOT, 'examples/flappylearning/assets/img/background.png')
|
||||
|
||||
fn test_load_image_loads_supported_file_contents() {
|
||||
img := load_image(background_path)!
|
||||
assert img.width > 0
|
||||
assert img.height > 0
|
||||
assert img.nr_channels == 4
|
||||
assert img.path == background_path
|
||||
}
|
||||
|
||||
fn test_load_image_returns_error_for_unsupported_file_contents() {
|
||||
file := os.join_path(os.vtmp_dir(), 'gg_unsupported_image_${os.getpid()}.jpg')
|
||||
os.write_file(file, 'not an image') or { panic(err) }
|
||||
defer {
|
||||
os.rm(file) or {}
|
||||
}
|
||||
load_image(file) or {
|
||||
assert err.msg().contains('stbi_image failed to load')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// vtest build: !docker-ubuntu-musl // needs GL/gl.h
|
||||
import os
|
||||
import gg
|
||||
|
||||
const vroot = @VEXEROOT
|
||||
const background_path = os.join_path(vroot, 'examples/flappylearning/assets/img/background.png')
|
||||
|
||||
fn test_get_cached_image_idx_bounds_checking() {
|
||||
mut ctx := gg.new_context(width: 100)
|
||||
assert ctx.get_cached_image_by_idx(-1).ok == false
|
||||
assert ctx.get_cached_image_by_idx(1).ok == false
|
||||
}
|
||||
|
||||
fn test_remove_cached_image_remove_and_get() {
|
||||
mut ctx := gg.new_context(width: 100)
|
||||
image := gg.Image{
|
||||
ok: true
|
||||
}
|
||||
idx := ctx.cache_image(image)
|
||||
assert ctx.get_cached_image_by_idx(idx).ok == true
|
||||
ctx.remove_cached_image_by_idx(idx)
|
||||
assert ctx.get_cached_image_by_idx(idx).ok == false
|
||||
}
|
||||
|
||||
fn test_remove_cached_images_by_id_keeps_other_ids_valid() {
|
||||
mut ctx := gg.new_context(width: 100)
|
||||
background_bytes := os.read_bytes(background_path)!
|
||||
prev := ctx.create_image_from_byte_array(background_bytes)!
|
||||
curr := ctx.create_image_from_byte_array(background_bytes)!
|
||||
assert prev.id == 0
|
||||
assert curr.id == 1
|
||||
assert ctx.get_cached_image_by_idx(prev.id).ok == true
|
||||
assert ctx.get_cached_image_by_idx(curr.id).ok == true
|
||||
ctx.remove_cached_image_by_idx(prev.id)
|
||||
ctx.remove_cached_image_by_idx(curr.id)
|
||||
assert ctx.get_cached_image_by_idx(prev.id).ok == false
|
||||
assert ctx.get_cached_image_by_idx(curr.id).ok == false
|
||||
}
|
||||
|
||||
fn test_new_context_sets_borderless_window_flag() {
|
||||
ctx := gg.new_context(
|
||||
width: 100
|
||||
borderless_window: true
|
||||
)
|
||||
assert ctx.window.borderless_window == true
|
||||
}
|
||||
|
||||
fn test_new_context_sets_texture_filter() {
|
||||
ctx := gg.new_context(
|
||||
width: 100
|
||||
texture_filter: .nearest
|
||||
)
|
||||
assert ctx.config.texture_filter == .nearest
|
||||
}
|
||||
|
||||
fn test_create_image_from_byte_array_loads_rgba_pixels() {
|
||||
mut ctx := gg.new_context(width: 100)
|
||||
background_bytes := os.read_bytes(background_path)!
|
||||
img := ctx.create_image_from_byte_array(background_bytes)!
|
||||
assert img.width > 0
|
||||
assert img.height > 0
|
||||
assert img.nr_channels == 4
|
||||
assert !isnil(img.data)
|
||||
assert img.texture_filter == .linear
|
||||
assert ctx.get_cached_image_by_idx(img.id).nr_channels == 4
|
||||
}
|
||||
|
||||
fn test_create_image_from_byte_array_uses_context_texture_filter() {
|
||||
mut ctx := gg.new_context(
|
||||
width: 100
|
||||
texture_filter: .nearest
|
||||
)
|
||||
background_bytes := os.read_bytes(background_path)!
|
||||
img := ctx.create_image_from_byte_array(background_bytes)!
|
||||
assert img.texture_filter == .nearest
|
||||
assert ctx.get_cached_image_by_idx(img.id).texture_filter == .nearest
|
||||
}
|
||||
|
||||
fn test_create_image_from_byte_array_with_filter_overrides_context_default() {
|
||||
mut ctx := gg.new_context(width: 100)
|
||||
background_bytes := os.read_bytes(background_path)!
|
||||
img := ctx.create_image_from_byte_array(background_bytes, texture_filter: .nearest)!
|
||||
assert img.texture_filter == .nearest
|
||||
assert ctx.get_cached_image_by_idx(img.id).texture_filter == .nearest
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module gg
|
||||
|
||||
// is_key_down returns whether the given key is currently pressed.
|
||||
// You can use this, if you do not want to implement your own key event handling.
|
||||
@[inline]
|
||||
pub fn (mut ctx Context) is_key_down(k KeyCode) bool {
|
||||
return ctx.pressed_keys[int(k)]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
module m4
|
||||
|
||||
/**********************************************************************
|
||||
* Simple vector/matrix graphic utility
|
||||
* Copyright (c) 2021 Dario Deledda. All rights reserved.
|
||||
* Use of this source code is governed by an MIT license
|
||||
* that can be found in the LICENSE file.
|
||||
**********************************************************************/
|
||||
import math
|
||||
|
||||
// Translate degrees to radians
|
||||
@[inline]
|
||||
pub fn rad(deg f32) f32 {
|
||||
return (math.pi / 180.0) * deg
|
||||
}
|
||||
|
||||
// Translate radians to degrees
|
||||
@[inline]
|
||||
pub fn deg(grad f32) f32 {
|
||||
return (180.0 / math.pi) * grad
|
||||
}
|
||||
|
||||
// calculate the Orthographic projection matrix
|
||||
pub fn ortho(left f32, right f32, bottom f32, top f32, z_near f32, z_far f32) Mat4 {
|
||||
rml := right - left
|
||||
rpl := right + left
|
||||
tmb := top - bottom
|
||||
tpb := top + bottom
|
||||
fmn := z_far - z_near
|
||||
fpn := z_far + z_near
|
||||
// vfmt off
|
||||
if fmn != 0 {
|
||||
return Mat4{
|
||||
e: [
|
||||
2 / rml, 0, 0, -(rpl / rml),
|
||||
0 , 2 / tmb, 0, -(tpb / tmb),
|
||||
0 , 0, 2 / fmn, -(fpn / fmn),
|
||||
0 , 0, 0, 1,
|
||||
]!
|
||||
}
|
||||
}
|
||||
return Mat4{
|
||||
e: [
|
||||
2 / rml, 0, 0, -(rpl / rml),
|
||||
0, 2 / tmb, 0, -(tpb / tmb),
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 1,
|
||||
]!
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Calculate the perspective matrix using (fov:fov, ar:aspect_ratio ,n:near_pane, f:far_plane) as parameters
|
||||
pub fn perspective(fov f32, ar f32, n f32, f f32) Mat4 {
|
||||
ctan := f32(1.0 / math.tan(fov * (f32(math.pi) / 360.0))) // for the FOV we use 360 instead 180
|
||||
// vfmt off
|
||||
return Mat4{
|
||||
e: [
|
||||
ctan / ar, 0, 0, 0,
|
||||
0, ctan, 0, 0,
|
||||
0, 0, (n + f) / (n - f), -1.0,
|
||||
0, 0, (2.0 * n * f) / (n - f), 0,
|
||||
]!
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Calculate the look-at matrix
|
||||
pub fn look_at(eye Vec4, center Vec4, up Vec4) Mat4 {
|
||||
f := (center - eye).normalize3()
|
||||
s := (f % up).normalize3()
|
||||
u := (s % f)
|
||||
// vfmt off
|
||||
return Mat4{e: [
|
||||
s.e[0], u.e[0], -f.e[0], 0,
|
||||
s.e[1], u.e[1], -f.e[1], 0,
|
||||
s.e[2], u.e[2], -f.e[2], 0,
|
||||
-(s * eye), -(u * eye), f * eye, 1,
|
||||
]!}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Get the complete transformation matrix for GLSL demos
|
||||
pub fn calc_tr_matrices(w f32, h f32, rx f32, ry f32, in_scale f32) Mat4 {
|
||||
proj := perspective(60, w / h, 0.01, 10.0)
|
||||
// vfmt off
|
||||
view := look_at(
|
||||
Vec4{ e: [f32(0.0), 1.5, 6, 0]! },
|
||||
Vec4{ e: [f32(0), 0, 0, 0]!},
|
||||
Vec4{ e: [f32(0), 1.0, 0, 0]!}
|
||||
)
|
||||
view_proj := view * proj
|
||||
|
||||
rxm := rotate(rad(rx), Vec4{ e: [f32(1), 0, 0, 0]! })
|
||||
rym := rotate(rad(ry), Vec4{ e: [f32(0), 1, 0, 0]! })
|
||||
scale_m := scale(Vec4{ e: [in_scale, in_scale, in_scale, 1]! })
|
||||
// vfmt on
|
||||
|
||||
model := rym * rxm
|
||||
res := (scale_m * model) * view_proj
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import gg.m4
|
||||
|
||||
// vfmt off
|
||||
pub fn test_m4() {
|
||||
unsafe {
|
||||
// Test Mat4
|
||||
mut a := m4.Mat4{ e: [
|
||||
f32(0), 1, 2, 3,
|
||||
4, 5, 6, 7,
|
||||
8, 9, 10, 11,
|
||||
12, 13, 14, 15,
|
||||
]!
|
||||
}
|
||||
mut b := m4.Mat4{}
|
||||
mut c := m4.Mat4{}
|
||||
|
||||
// equal test
|
||||
assert a.e == [
|
||||
f32(0), 1, 2, 3,
|
||||
4, 5, 6, 7,
|
||||
8, 9, 10, 11,
|
||||
12, 13, 14, 15,
|
||||
]!
|
||||
|
||||
// copy test
|
||||
b.copy(a)
|
||||
assert a.e == b.e
|
||||
|
||||
// test: transpose, scale
|
||||
assert b.transpose().mul_scalar(2.0).mul_scalar(0.5).transpose().e == a.e
|
||||
assert b.sum_all() == 120.0
|
||||
|
||||
// test rows/columns set/get
|
||||
for i in 0 .. 4 {
|
||||
b = m4.zero_m4()
|
||||
b.set_row(i, m4.Vec4{ e: [f32(1.0), 2, 3, 4]! })
|
||||
assert b.get_f(0, i) == 1.0
|
||||
assert b.get_f(1, i) == 2.0
|
||||
assert b.get_f(2, i) == 3.0
|
||||
assert b.get_f(3, i) == 4.0
|
||||
// println(b)
|
||||
c = m4.zero_m4()
|
||||
c.set_col(i, m4.Vec4{ e: [f32(1.0), 2, 3, 4]! })
|
||||
assert c.get_f(i, 0) == 1.0
|
||||
assert c.get_f(i, 1) == 2.0
|
||||
assert c.get_f(i, 2) == 3.0
|
||||
assert c.get_f(i, 3) == 4.0
|
||||
// println(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_swap_col_row() {
|
||||
unsafe {
|
||||
// swap_col / swap_row
|
||||
b := m4.Mat4{ e: [
|
||||
f32(1), 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 15, 16,
|
||||
]!
|
||||
}
|
||||
b.swap_col(0, 2)
|
||||
assert b.e == [
|
||||
f32(3), 2, 1, 4,
|
||||
7, 6, 5, 8,
|
||||
11, 10, 9, 12,
|
||||
15, 14, 13, 16,
|
||||
]!
|
||||
b = m4.Mat4{ e: [
|
||||
f32(1), 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 15, 16,
|
||||
]!
|
||||
}
|
||||
b.swap_row(0, 2)
|
||||
assert b.e == [
|
||||
f32(9), 10, 11, 12,
|
||||
5, 6, 7, 8,
|
||||
1, 2, 3, 4,
|
||||
13, 14, 15, 16,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
fn test_sum_sub() {
|
||||
unsafe {
|
||||
// test sum/sub
|
||||
b := m4.unit_m4()
|
||||
c := m4.unit_m4()
|
||||
assert m4.sub(m4.add(b, c), b).e == m4.unit_m4().e
|
||||
assert (b + c - b).e == m4.unit_m4().e
|
||||
}
|
||||
}
|
||||
|
||||
fn test_transpose() {
|
||||
unsafe {
|
||||
b := m4.Mat4{ e: [
|
||||
f32(0), 1, 2, 3,
|
||||
4, 5, 6, 7,
|
||||
8, 9, 10, 11,
|
||||
12, 13, 14, 15,
|
||||
]!
|
||||
}
|
||||
assert b.transpose().transpose().e == b.e
|
||||
}
|
||||
}
|
||||
|
||||
fn test_multiplication() {
|
||||
unsafe {
|
||||
b := m4.Mat4{ e: [
|
||||
f32(1), 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, 3, 0,
|
||||
0, 0, 0, 4,
|
||||
]!
|
||||
}
|
||||
c := m4.Mat4{ e: [
|
||||
f32(1), 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 15, 16,
|
||||
]!
|
||||
}
|
||||
|
||||
assert (c * c).e == [
|
||||
f32(90),100,110,120,
|
||||
202,228,254,280,
|
||||
314,356,398,440,
|
||||
426,484,542,600,
|
||||
]!
|
||||
|
||||
assert m4.mul(c, c).e == [
|
||||
f32(90),100,110,120,
|
||||
202,228,254,280,
|
||||
314,356,398,440,
|
||||
426,484,542,600,
|
||||
]!
|
||||
|
||||
assert m4.mul(b, c).e == [
|
||||
f32(1), 2, 3, 4,
|
||||
10, 12, 14, 16,
|
||||
27, 30, 33, 36,
|
||||
52, 56, 60, 64,
|
||||
]!
|
||||
|
||||
assert (b * c).e == [
|
||||
f32(1), 2, 3, 4,
|
||||
10, 12, 14, 16,
|
||||
27, 30, 33, 36,
|
||||
52, 56, 60, 64,
|
||||
]!
|
||||
|
||||
assert m4.det(b) == 24
|
||||
}
|
||||
}
|
||||
|
||||
fn test_det() {
|
||||
unsafe {
|
||||
b := m4.Mat4{ e: [
|
||||
f32(5), 6, 6, 8,
|
||||
2, 2, 2, 8,
|
||||
6, 6, 2, 8,
|
||||
2, 3, 6, 7,
|
||||
]!
|
||||
}
|
||||
assert m4.det(b) == -8
|
||||
|
||||
c := m4.Mat4{ e: [
|
||||
f32(1), 8, 2, 3,
|
||||
8, 2, 3, 1,
|
||||
2, 3, 3, 2,
|
||||
3, 1, 2, 4,
|
||||
]!
|
||||
}
|
||||
// println("*** INVERSE ****")
|
||||
// println(m4.mul(b.inverse(),b))
|
||||
// println(m4.clean_small(m4.mul(c.inverse(),c)))
|
||||
// println("****************")
|
||||
assert m4.mul(b.inverse(), b).e == m4.unit_m4().e
|
||||
assert m4.mul(c.inverse(), c).is_equal(m4.unit_m4())
|
||||
}
|
||||
}
|
||||
|
||||
fn test_vec4() {
|
||||
// Test Vec4
|
||||
// println("*** Vector4 ****")
|
||||
assert m4.vec3(1,2,3) == m4.Vec4{[f32(1), 2, 3, 1]!}
|
||||
mut v := m4.Vec4{[f32(1), 2, 3, 4]!}
|
||||
assert v * v.inv() == 4
|
||||
assert v.mul_scalar(1.0 / v.mod()).mod() == 1
|
||||
assert v + m4.Vec4{ e: [f32(5), 6, 7, 8]! } == m4.Vec4{ e: [f32(6), 8, 10, 12]! }
|
||||
assert v - m4.Vec4{ e: [f32(1), 2, 3, 4]! } == m4.Vec4{ e: [f32(0), 0, 0, 0]! }
|
||||
assert v.mul_vec4(m4.Vec4{ e: [f32(2), 2, 2, 2]! }) == m4.Vec4{ e: [f32(2), 4, 6, 8]! }
|
||||
assert f32_abs(v.normalize().mod() - 1) < m4.precision
|
||||
v = m4.Vec4{[f32(1), 2, 3, 0]!}
|
||||
assert f32_abs(v.normalize3().mod3() - 1) < m4.precision
|
||||
assert f32_abs(v.normalize3().mod() - 1) < m4.precision
|
||||
// cross product
|
||||
// x y z
|
||||
// 1 2 3 ==> -3 6 -3 0
|
||||
// 4 5 6
|
||||
// println(m4.Vec4{[f32(1),2,3,2]!} % m4.Vec4{[f32(4),5,6,2]!})
|
||||
assert m4.Vec4{[f32(1), 2, 3, 0]!} % m4.Vec4{[f32(4), 5, 6, 0 ]!} == m4.Vec4{[ f32(-3), 6, -3, 0, ]!}
|
||||
assert m4.Vec4{[f32(1), 2, 3, 13]!} % m4.Vec4{[f32(4), 5, 6, 11]!} == m4.Vec4{[ f32(-3), 6, -3, 0, ]!}
|
||||
// matrix * vector
|
||||
a := m4.Mat4{ e: [
|
||||
f32(1), 2, 3, 4
|
||||
5, 6, 7, 8
|
||||
9, 10, 11, 12
|
||||
13, 14, 15, 16
|
||||
]!
|
||||
}
|
||||
assert m4.mul_vec(a, m4.Vec4{[f32(1), 2, 3, 4]!}) == m4.Vec4{[ f32(30), 70, 110,150, ]!}
|
||||
// Rotation
|
||||
// println("*** Rotation ****")
|
||||
rotx := m4.rotate(m4.rad(-90), m4.Vec4{ e: [f32(1.0), 0, 0, 0]! }).clean()
|
||||
roty := m4.rotate(m4.rad(-90), m4.Vec4{ e: [f32(0), 1.0, 0, 0]! }).clean()
|
||||
rotz := m4.rotate(m4.rad(-90), m4.Vec4{ e: [f32(0), 0, 1, 0]! }).clean()
|
||||
// println( rotx )
|
||||
// println( roty )
|
||||
// println( rotz )
|
||||
// println( m4.mul_vec(rotx, m4.Vec4{e:[f32(0),0,1,0]!}).clean())
|
||||
assert m4.mul_vec(roty, m4.Vec4{ e: [f32(1.0), 0.0, 0, 0]! }).clean() == m4.Vec4{ e: [f32(0), 0.0, -1, 0]! }
|
||||
assert m4.mul_vec(rotz, m4.Vec4{ e: [f32(1.0), 0.0, 0, 0]! }).clean() == m4.Vec4{ e: [f32(0), 1, 0, 0]! }
|
||||
assert m4.mul_vec(rotx, m4.Vec4{ e: [f32(0), 0, 1, 0]! }).clean() == m4.Vec4{ e: [f32(0), -1, 0, 0]! }
|
||||
// println("****************")
|
||||
}
|
||||
|
||||
fn test_proj() {
|
||||
ort := m4.ortho(0,300,0,200,0,0)
|
||||
assert m4.mul_vec(ort, m4.Vec4{[ f32(150), 100, 0, 1]!}).is_equal(m4.Vec4{[ f32(0), 0, 0, 1]!})
|
||||
assert m4.mul_vec(ort, m4.Vec4{[ f32(0), 0, 0, 1]!}).is_equal(m4.Vec4{[ f32(-1), -1, 0, 1]!})
|
||||
assert m4.mul_vec(ort, m4.Vec4{[ f32(300), 200, 0, 1]!}).is_equal(m4.Vec4{[ f32(1), 1, 0, 1]!})
|
||||
}
|
||||
// vfmt on
|
||||
@@ -0,0 +1,590 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
* Simply vector/matrix utility
|
||||
*
|
||||
* Copyright (c) 2021 Dario Deledda. All rights reserved.
|
||||
* Use of this source code is governed by an MIT license
|
||||
* that can be found in the LICENSE file.
|
||||
*
|
||||
* TODO:
|
||||
**********************************************************************/
|
||||
module m4
|
||||
|
||||
import math
|
||||
|
||||
pub union Mat4 {
|
||||
pub mut:
|
||||
e [16]f32
|
||||
f [4][4]f32
|
||||
}
|
||||
|
||||
pub const precision = f32(10e-7)
|
||||
|
||||
// default precision for the module
|
||||
|
||||
/*********************************************************************
|
||||
*
|
||||
* Utility
|
||||
*
|
||||
*********************************************************************/
|
||||
// String representation of the matrix
|
||||
pub fn (x Mat4) str() string {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
'|${x.e[0]: -6.3},${x.e[1]: -6.3},${x.e[2]: -6.3},${x.e[3]: -6.3}|\n' +
|
||||
'|${x.e[4]: -6.3},${x.e[5]: -6.3},${x.e[6]: -6.3},${x.e[7]: -6.3}|\n' +
|
||||
'|${x.e[8]: -6.3},${x.e[9]: -6.3},${x.e[10]:-6.3},${x.e[11]:-6.3}|\n' +
|
||||
'|${x.e[12]:-6.3},${x.e[13]:-6.3},${x.e[14]:-6.3},${x.e[15]:-6.3}|'
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Remove all the raw zeros
|
||||
@[direct_array_access]
|
||||
pub fn (a Mat4) clean() Mat4 {
|
||||
unsafe {
|
||||
x := Mat4{}
|
||||
for c, value in a.e {
|
||||
if f32_abs(value) < precision {
|
||||
x.e[c] = 0
|
||||
} else {
|
||||
x.e[c] = value
|
||||
}
|
||||
}
|
||||
return x
|
||||
}
|
||||
return zero_m4() // workaround -cstrict
|
||||
}
|
||||
|
||||
// Sum all the elements of the matrix
|
||||
pub fn (x Mat4) sum_all() f32 {
|
||||
mut res := f32(0)
|
||||
for v in unsafe { x.e } {
|
||||
res += v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Check if two matrix are equal using module precision
|
||||
@[direct_array_access]
|
||||
pub fn (x Mat4) is_equal(y Mat4) bool {
|
||||
unsafe {
|
||||
for c, value in x.e {
|
||||
if f32_abs(value - y.e[c]) > precision {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
// Set/Get values
|
||||
//-------------------------------------
|
||||
// Get an element of the matrix using [0..15] indexes, one dimension
|
||||
pub fn (x Mat4) get_e(elem_index int) f32 {
|
||||
unsafe {
|
||||
return x.e[elem_index]
|
||||
}
|
||||
}
|
||||
|
||||
// Get an element of the matrix using [0..3][0..3] indexes, two dimension
|
||||
pub fn (x Mat4) get_f(index_col int, index_row int) f32 {
|
||||
unsafe {
|
||||
return x.e[int(u32(index_row) << 2) + index_col]
|
||||
}
|
||||
}
|
||||
|
||||
// Set an element of the matrix using [0..15] indexes, one dimension
|
||||
pub fn (mut x Mat4) set_e(index int, value f32) {
|
||||
unsafe {
|
||||
x.e[index] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Set an element of the matrix using [0..3][0..3] indexes, two dimension
|
||||
pub fn (mut x Mat4) set_f(index_col int, index_row int, value f32) {
|
||||
unsafe {
|
||||
x.e[int(u32(index_row) << 2) + index_col] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Copy a matrix elements from another matrix
|
||||
pub fn (mut x Mat4) copy(y Mat4) {
|
||||
unsafe {
|
||||
x.e = [
|
||||
// vfmt off
|
||||
y.e[0 ], y.e[1 ], y.e[2 ], y.e[3 ],
|
||||
y.e[4 ], y.e[5 ], y.e[6 ], y.e[7 ],
|
||||
y.e[8 ], y.e[9 ], y.e[10], y.e[11],
|
||||
y.e[12], y.e[13], y.e[14], y.e[15],
|
||||
// vfmt on
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// Set the trace of the matrix using a vec4
|
||||
pub fn (mut x Mat4) set_trace(v3 Vec4) {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
x.e[0 ] = v3.e[0]
|
||||
x.e[5 ] = v3.e[1]
|
||||
x.e[10] = v3.e[2]
|
||||
x.e[15] = v3.e[3]
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
// Get the trace of the matrix
|
||||
pub fn (x Mat4) get_trace() Vec4 {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
return Vec4{ e: [ x.e[0], x.e[5], x.e[10], x.e[15], ]! }
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
// Set all the matrix elements to value
|
||||
pub fn (mut x Mat4) set_f32(value f32) {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
x.e = [
|
||||
value, value, value, value,
|
||||
value, value, value, value,
|
||||
value, value, value, value,
|
||||
value, value, value, value,
|
||||
]!
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
// Rows/Column access
|
||||
//-------------------------------------
|
||||
// Set the row as the input vec4
|
||||
@[direct_array_access; unsafe]
|
||||
pub fn (mut x Mat4) set_row(row int, v3 Vec4) {
|
||||
unsafe {
|
||||
x.e[row * 4 + 0] = v3.e[0]
|
||||
x.e[row * 4 + 1] = v3.e[1]
|
||||
x.e[row * 4 + 2] = v3.e[2]
|
||||
x.e[row * 4 + 3] = v3.e[3]
|
||||
}
|
||||
}
|
||||
|
||||
// Get a row from a matrix
|
||||
@[direct_array_access; unsafe]
|
||||
pub fn (x Mat4) get_row(row int) Vec4 {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
return Vec4{ e: [ x.e[row * 4], x.e[row * 4 + 1], x.e[row * 4 + 2], x.e[row * 4 + 3], ]! }
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
// Set the column as the input vec4
|
||||
@[direct_array_access; unsafe]
|
||||
pub fn (mut x Mat4) set_col(col int, v3 Vec4) {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
x.e[col ] = v3.e[0]
|
||||
x.e[col + 4 ] = v3.e[1]
|
||||
x.e[col + 8 ] = v3.e[2]
|
||||
x.e[col + 12] = v3.e[3]
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
// Get a column from a matrix
|
||||
@[direct_array_access; unsafe]
|
||||
pub fn (x Mat4) get_col(col int) Vec4 {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
return Vec4{ e: [ x.e[col], x.e[col + 4 ], x.e[col + 8 ], x.e[col + 12], ]!}
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
// Swap two columns in the matrix
|
||||
@[direct_array_access; unsafe]
|
||||
pub fn (mut x Mat4) swap_col(col1 int, col2 int) {
|
||||
unsafe {
|
||||
// vfmt off
|
||||
v0 := x.e[col1 ]
|
||||
v1 := x.e[col1 + 4 ]
|
||||
v2 := x.e[col1 + 8 ]
|
||||
v3 := x.e[col1 + 12]
|
||||
|
||||
x.e[col1 ] = x.e[col2 ]
|
||||
x.e[col1 + 4 ] = x.e[col2 + 4 ]
|
||||
x.e[col1 + 8 ] = x.e[col2 + 8 ]
|
||||
x.e[col1 + 12] = x.e[col2 + 12]
|
||||
|
||||
x.e[col2 ] = v0
|
||||
x.e[col2 + 4 ] = v1
|
||||
x.e[col2 + 8 ] = v2
|
||||
x.e[col2 + 12] = v3
|
||||
// vfmt on
|
||||
}
|
||||
}
|
||||
|
||||
// Swap two rows in the matrix
|
||||
@[direct_array_access; unsafe]
|
||||
pub fn (mut x Mat4) swap_row(row1 int, row2 int) {
|
||||
unsafe {
|
||||
v0 := x.e[row1 * 4 + 0]
|
||||
v1 := x.e[row1 * 4 + 1]
|
||||
v2 := x.e[row1 * 4 + 2]
|
||||
v3 := x.e[row1 * 4 + 3]
|
||||
|
||||
x.e[row1 * 4 + 0] = x.e[row2 * 4 + 0]
|
||||
x.e[row1 * 4 + 1] = x.e[row2 * 4 + 1]
|
||||
x.e[row1 * 4 + 2] = x.e[row2 * 4 + 2]
|
||||
x.e[row1 * 4 + 3] = x.e[row2 * 4 + 3]
|
||||
|
||||
x.e[row2 * 4 + 0] = v0
|
||||
x.e[row2 * 4 + 1] = v1
|
||||
x.e[row2 * 4 + 2] = v2
|
||||
x.e[row2 * 4 + 3] = v3
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
// Modify data
|
||||
//-------------------------------------
|
||||
// Transpose the matrix
|
||||
pub fn (x Mat4) transpose() Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
x.e[0 ], x.e[4 ], x.e[8 ], x.e[12],
|
||||
x.e[1 ], x.e[5 ], x.e[9 ], x.e[13],
|
||||
x.e[2 ], x.e[6 ], x.e[10], x.e[14],
|
||||
x.e[3 ], x.e[7 ], x.e[11], x.e[15],
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Multiply the all the elements of the matrix by a scalar
|
||||
pub fn (x Mat4) mul_scalar(s f32) Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
x.e[0 ] * s, x.e[1 ] * s, x.e[2 ] * s, x.e[3 ] * s,
|
||||
x.e[4 ] * s, x.e[5 ] * s, x.e[6 ] * s, x.e[7 ] * s,
|
||||
x.e[8 ] * s, x.e[9 ] * s, x.e[10] * s, x.e[11] * s,
|
||||
x.e[12] * s, x.e[13] * s, x.e[14] * s, x.e[15] * s,
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
*
|
||||
* Init/set
|
||||
*
|
||||
*********************************************************************/
|
||||
// Return a zero matrix
|
||||
pub fn zero_m4() Mat4 {
|
||||
// vfmt off
|
||||
return Mat4{ e: [
|
||||
f32(0), 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
]!}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Return a unity matrix
|
||||
pub fn unit_m4() Mat4 {
|
||||
// vfmt off
|
||||
return Mat4{ e: [
|
||||
f32(1), 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
]!}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Return a matrix initialized with value
|
||||
pub fn set_m4(value f32) Mat4 {
|
||||
// vfmt off
|
||||
return Mat4{ e: [
|
||||
value, value, value, value,
|
||||
value, value, value, value,
|
||||
value, value, value, value,
|
||||
value, value, value, value,
|
||||
]!}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
*
|
||||
* Math
|
||||
*
|
||||
*********************************************************************/
|
||||
|
||||
// Sum of matrix, operator +
|
||||
pub fn (a Mat4) + (b Mat4) Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
a.e[0 ] + b.e[0 ], a.e[1 ] + b.e[1 ], a.e[2 ] + b.e[2 ], a.e[3 ] + b.e[3 ],
|
||||
a.e[4 ] + b.e[4 ], a.e[5 ] + b.e[5 ], a.e[6 ] + b.e[6 ], a.e[7 ] + b.e[7 ],
|
||||
a.e[8 ] + b.e[8 ], a.e[9 ] + b.e[9 ], a.e[10] + b.e[10], a.e[11] + b.e[11],
|
||||
a.e[12] + b.e[12], a.e[13] + b.e[13], a.e[14] + b.e[14], a.e[15] + b.e[15],
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Subtraction of matrix, operator -
|
||||
pub fn (a Mat4) - (b Mat4) Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
a.e[0 ] - b.e[0 ], a.e[1 ] - b.e[1 ], a.e[2 ] - b.e[2 ], a.e[3 ] - b.e[3 ],
|
||||
a.e[4 ] - b.e[4 ], a.e[5 ] - b.e[5 ], a.e[6 ] - b.e[6 ], a.e[7 ] - b.e[7 ],
|
||||
a.e[8 ] - b.e[8 ], a.e[9 ] - b.e[9 ], a.e[10] - b.e[10], a.e[11] - b.e[11],
|
||||
a.e[12] - b.e[12], a.e[13] - b.e[13], a.e[14] - b.e[14], a.e[15] - b.e[15],
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Multiplication of matrix, operator *
|
||||
pub fn (a Mat4) * (b Mat4) Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
a.f[0][0] * b.f[0][0] + a.f[0][1] * b.f[1][0] + a.f[0][2] * b.f[2][0] + a.f[0][3] * b.f[3][0], // [0][0]
|
||||
a.f[0][0] * b.f[0][1] + a.f[0][1] * b.f[1][1] + a.f[0][2] * b.f[2][1] + a.f[0][3] * b.f[3][1], // [0][1]
|
||||
a.f[0][0] * b.f[0][2] + a.f[0][1] * b.f[1][2] + a.f[0][2] * b.f[2][2] + a.f[0][3] * b.f[3][2], // [0][2]
|
||||
a.f[0][0] * b.f[0][3] + a.f[0][1] * b.f[1][3] + a.f[0][2] * b.f[2][3] + a.f[0][3] * b.f[3][3], // [0][3]
|
||||
|
||||
a.f[1][0] * b.f[0][0] + a.f[1][1] * b.f[1][0] + a.f[1][2] * b.f[2][0] + a.f[1][3] * b.f[3][0], // [1][0]
|
||||
a.f[1][0] * b.f[0][1] + a.f[1][1] * b.f[1][1] + a.f[1][2] * b.f[2][1] + a.f[1][3] * b.f[3][1], // [1][1]
|
||||
a.f[1][0] * b.f[0][2] + a.f[1][1] * b.f[1][2] + a.f[1][2] * b.f[2][2] + a.f[1][3] * b.f[3][2], // [1][2]
|
||||
a.f[1][0] * b.f[0][3] + a.f[1][1] * b.f[1][3] + a.f[1][2] * b.f[2][3] + a.f[1][3] * b.f[3][3], // [1][3]
|
||||
|
||||
a.f[2][0] * b.f[0][0] + a.f[2][1] * b.f[1][0] + a.f[2][2] * b.f[2][0] + a.f[2][3] * b.f[3][0], // [2][0]
|
||||
a.f[2][0] * b.f[0][1] + a.f[2][1] * b.f[1][1] + a.f[2][2] * b.f[2][1] + a.f[2][3] * b.f[3][1], // [2][1]
|
||||
a.f[2][0] * b.f[0][2] + a.f[2][1] * b.f[1][2] + a.f[2][2] * b.f[2][2] + a.f[2][3] * b.f[3][2], // [2][2]
|
||||
a.f[2][0] * b.f[0][3] + a.f[2][1] * b.f[1][3] + a.f[2][2] * b.f[2][3] + a.f[2][3] * b.f[3][3], // [2][3]
|
||||
|
||||
a.f[3][0] * b.f[0][0] + a.f[3][1] * b.f[1][0] + a.f[3][2] * b.f[2][0] + a.f[3][3] * b.f[3][0], // [3][0]
|
||||
a.f[3][0] * b.f[0][1] + a.f[3][1] * b.f[1][1] + a.f[3][2] * b.f[2][1] + a.f[3][3] * b.f[3][1], // [3][1]
|
||||
a.f[3][0] * b.f[0][2] + a.f[3][1] * b.f[1][2] + a.f[3][2] * b.f[2][2] + a.f[3][3] * b.f[3][2], // [3][2]
|
||||
a.f[3][0] * b.f[0][3] + a.f[3][1] * b.f[1][3] + a.f[3][2] * b.f[2][3] + a.f[3][3] * b.f[3][3], // [3][3]
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Sum of matrix function
|
||||
pub fn add(a Mat4, b Mat4) Mat4 {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Subtraction of matrix function
|
||||
pub fn sub(a Mat4, b Mat4) Mat4 {
|
||||
return a - b
|
||||
}
|
||||
|
||||
// Multiplication of matrix function
|
||||
pub fn mul(a Mat4, b Mat4) Mat4 {
|
||||
return a * b
|
||||
}
|
||||
|
||||
// Multiply a Matrix by a vector
|
||||
pub fn mul_vec(a Mat4, v Vec4) Vec4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Vec4{ e: [
|
||||
a.e[0 ] * v.e[0] + a.e[1 ] * v.e[1] + a.e[2 ] * v.e[2] + a.e[3 ] * v.e[3],
|
||||
a.e[4 ] * v.e[0] + a.e[5 ] * v.e[1] + a.e[6 ] * v.e[2] + a.e[7 ] * v.e[3],
|
||||
a.e[8 ] * v.e[0] + a.e[9 ] * v.e[1] + a.e[10] * v.e[2] + a.e[11] * v.e[3],
|
||||
a.e[12] * v.e[0] + a.e[13] * v.e[1] + a.e[14] * v.e[2] + a.e[15] * v.e[3],
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Calculate the determinant of the Matrix
|
||||
pub fn det(x Mat4) f32 {
|
||||
unsafe {
|
||||
mut t := [6]f32{}
|
||||
x00 := x.f[0][0]
|
||||
x10 := x.f[1][0]
|
||||
x20 := x.f[2][0]
|
||||
x30 := x.f[3][0]
|
||||
x01 := x.f[0][1]
|
||||
x11 := x.f[1][1]
|
||||
x21 := x.f[2][1]
|
||||
x31 := x.f[3][1]
|
||||
x02 := x.f[0][2]
|
||||
x12 := x.f[1][2]
|
||||
x22 := x.f[2][2]
|
||||
x32 := x.f[3][2]
|
||||
x03 := x.f[0][3]
|
||||
x13 := x.f[1][3]
|
||||
x23 := x.f[2][3]
|
||||
x33 := x.f[3][3]
|
||||
|
||||
t[0] = x22 * x33 - x23 * x32
|
||||
t[1] = x12 * x33 - x13 * x32
|
||||
t[2] = x12 * x23 - x13 * x22
|
||||
t[3] = x02 * x33 - x03 * x32
|
||||
t[4] = x02 * x23 - x03 * x22
|
||||
t[5] = x02 * x13 - x03 * x12
|
||||
// vfmt off
|
||||
return
|
||||
x00 * (x11 * t[0] - x21 * t[1] + x31 * t[2]) -
|
||||
x10 * (x01 * t[0] - x21 * t[3] + x31 * t[4]) +
|
||||
x20 * (x01 * t[1] - x11 * t[3] + x31 * t[5]) -
|
||||
x30 * (x01 * t[2] - x11 * t[4] + x21 * t[5])
|
||||
// vfmt on
|
||||
}
|
||||
return 0 // workaround -cstrict
|
||||
}
|
||||
|
||||
// Calculate the inverse of the Matrix
|
||||
pub fn (x Mat4) inverse() Mat4 {
|
||||
unsafe {
|
||||
mut t := [6]f32{}
|
||||
mut inv_det := f32(0)
|
||||
|
||||
// vfmt off
|
||||
a := x.f[0][0]
|
||||
b := x.f[1][0]
|
||||
c := x.f[2][0]
|
||||
d := x.f[3][0]
|
||||
e := x.f[0][1]
|
||||
f := x.f[1][1]
|
||||
g := x.f[2][1]
|
||||
h := x.f[3][1]
|
||||
i := x.f[0][2]
|
||||
j := x.f[1][2]
|
||||
k := x.f[2][2]
|
||||
l := x.f[3][2]
|
||||
m := x.f[0][3]
|
||||
n := x.f[1][3]
|
||||
o := x.f[2][3]
|
||||
p := x.f[3][3]
|
||||
|
||||
t[0] = k * p - o * l
|
||||
t[1] = j * p - n * l
|
||||
t[2] = j * o - n * k
|
||||
t[3] = i * p - m * l
|
||||
t[4] = i * o - m * k
|
||||
t[5] = i * n - m * j
|
||||
|
||||
mut dest := Mat4{}
|
||||
dest.f[0][0] = f * t[0] - g * t[1] + h * t[2]
|
||||
dest.f[0][1] = -(e * t[0] - g * t[3] + h * t[4])
|
||||
dest.f[0][2] = e * t[1] - f * t[3] + h * t[5]
|
||||
dest.f[0][3] = -(e * t[2] - f * t[4] + g * t[5])
|
||||
|
||||
dest.f[1][0] = -(b * t[0] - c * t[1] + d * t[2])
|
||||
dest.f[1][1] = a * t[0] - c * t[3] + d * t[4]
|
||||
dest.f[1][2] = -(a * t[1] - b * t[3] + d * t[5])
|
||||
dest.f[1][3] = a * t[2] - b * t[4] + c * t[5]
|
||||
|
||||
t[0] = g * p - o * h
|
||||
t[1] = f * p - n * h
|
||||
t[2] = f * o - n * g
|
||||
t[3] = e * p - m * h
|
||||
t[4] = e * o - m * g
|
||||
t[5] = e * n - m * f
|
||||
|
||||
dest.f[2][0] = b * t[0] - c * t[1] + d * t[2]
|
||||
dest.f[2][1] = -(a * t[0] - c * t[3] + d * t[4])
|
||||
dest.f[2][2] = a * t[1] - b * t[3] + d * t[5]
|
||||
dest.f[2][3] = -(a * t[2] - b * t[4] + c * t[5])
|
||||
|
||||
t[0] = g * l - k * h
|
||||
t[1] = f * l - j * h
|
||||
t[2] = f * k - j * g
|
||||
t[3] = e * l - i * h
|
||||
t[4] = e * k - i * g
|
||||
t[5] = e * j - i * f
|
||||
|
||||
dest.f[3][0] = -(b * t[0] - c * t[1] + d * t[2])
|
||||
dest.f[3][1] = a * t[0] - c * t[3] + d * t[4]
|
||||
dest.f[3][2] = -(a * t[1] - b * t[3] + d * t[5])
|
||||
dest.f[3][3] = a * t[2] - b * t[4] + c * t[5]
|
||||
// vfmt on
|
||||
|
||||
tmp := (a * dest.f[0][0] + b * dest.f[0][1] + c * dest.f[0][2] + d * dest.f[0][3])
|
||||
if tmp != 0 {
|
||||
inv_det = f32(1.0) / tmp
|
||||
}
|
||||
return dest.mul_scalar(inv_det)
|
||||
}
|
||||
return zero_m4()
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
*
|
||||
* Transformations
|
||||
*
|
||||
*********************************************************************/
|
||||
|
||||
// Get a rotation matrix using w as rotation axis vector, the angle is in radians
|
||||
pub fn rotate(angle f32, w Vec4) Mat4 {
|
||||
cs := f32(math.cos(angle))
|
||||
sn := f32(math.sin(angle))
|
||||
cv := f32(1.0) - cs
|
||||
axis := w.normalize3()
|
||||
unsafe {
|
||||
ax := axis.e[0]
|
||||
ay := axis.e[1]
|
||||
az := axis.e[2]
|
||||
|
||||
// vfmt off
|
||||
return Mat4{ e: [
|
||||
(ax * ax * cv) + cs, (ax * ay * cv) + az * sn, (ax * az * cv) - ay * sn, 0,
|
||||
(ay * ax * cv) - az * sn, (ay * ay * cv) + cs, (ay * az * cv) + ax * sn, 0,
|
||||
(az * ax * cv) + ay * sn, (az * ay * cv) - ax * sn, (az * az * cv) + cs, 0,
|
||||
0, 0, 0, 1,
|
||||
]!}
|
||||
// vfmt on
|
||||
}
|
||||
return zero_m4()
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
*
|
||||
* Graphic
|
||||
*
|
||||
*********************************************************************/
|
||||
// Get a matrix translated by a vector w
|
||||
pub fn (x Mat4) translate(w Vec4) Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
x.e[0], x.e[1], x.e[2 ], x.e[3 ],
|
||||
x.e[4], x.e[5], x.e[6 ], x.e[7 ],
|
||||
x.e[8], x.e[9], x.e[10], x.e[11],
|
||||
x.e[12] + w.e[0], x.e[13] + w.e[1], x.e[14] + w.e[2], x.e[15],
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
|
||||
// Get a scale matrix, the scale vector is w, only xyz are evaluated.
|
||||
pub fn scale(w Vec4) Mat4 {
|
||||
// vfmt off
|
||||
return unsafe {
|
||||
Mat4{ e: [
|
||||
w.e[0], 0, 0, 0,
|
||||
0, w.e[1], 0, 0,
|
||||
0, 0, w.e[2], 0,
|
||||
0, 0, 0, 1,
|
||||
]!}
|
||||
}
|
||||
// vfmt on
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/**********************************************************************
|
||||
* Simple vector/matrix utility
|
||||
* Copyright (c) 2024 Dario Deledda. All rights reserved.
|
||||
* Use of this source code is governed by an MIT license
|
||||
* that can be found in the LICENSE file.
|
||||
**********************************************************************/
|
||||
module m4
|
||||
|
||||
import math
|
||||
|
||||
pub struct Vec4 {
|
||||
pub mut:
|
||||
e [4]f32
|
||||
}
|
||||
|
||||
// str returns a `string` representation of `Vec4`.
|
||||
pub fn (x Vec4) str() string {
|
||||
return '|${x.e[0]:-6.3},${x.e[1]:-6.3},${x.e[2]:-6.3},${x.e[3]:-6.3}|'
|
||||
}
|
||||
|
||||
// vec3 creates a Vec4 value, passing x,y,z as parameters. The w element is set to 1
|
||||
@[inline]
|
||||
pub fn vec3(x f32, y f32, z f32) Vec4 {
|
||||
return Vec4{
|
||||
e: [x, y, z, 1]!
|
||||
}
|
||||
}
|
||||
|
||||
// vec4 creates a Vec4 value, based on the x,y,z,w parameters
|
||||
@[inline]
|
||||
pub fn vec4(x f32, y f32, z f32, w f32) Vec4 {
|
||||
return Vec4{
|
||||
e: [x, y, z, w]!
|
||||
}
|
||||
}
|
||||
|
||||
// is_equal checks if two vector are equal using the module precision (10e-7)
|
||||
@[direct_array_access]
|
||||
pub fn (x Vec4) is_equal(y Vec4) bool {
|
||||
unsafe {
|
||||
for c, value in x.e {
|
||||
if f32_abs(value - y.e[c]) > precision {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// clean returns a new vector, based on `x`, but with all the values < precision, set to 0
|
||||
@[direct_array_access]
|
||||
pub fn (x Vec4) clean() Vec4 {
|
||||
mut n := x
|
||||
for c, value in x.e {
|
||||
if f32_abs(value) < precision {
|
||||
n.e[c] = 0
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// copy sets all elements of `x` to `value`
|
||||
pub fn (mut x Vec4) copy(value f32) {
|
||||
x.e = [value, value, value, value]!
|
||||
}
|
||||
|
||||
// mul_scalar returns the result of multiplying the vector `x`, by the scalar `value`
|
||||
@[inline]
|
||||
pub fn (x Vec4) mul_scalar(value f32) Vec4 {
|
||||
return Vec4{
|
||||
e: [x.e[0] * value, x.e[1] * value, x.e[2] * value, x.e[3] * value]!
|
||||
}
|
||||
}
|
||||
|
||||
// inv returns the reciprocal of the vector `x`
|
||||
pub fn (x Vec4) inv() Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
if x.e[0] != 0 { 1.0 / x.e[0] } else { 0 },
|
||||
if x.e[1] != 0 { 1.0 / x.e[1] } else { 0 },
|
||||
if x.e[2] != 0 { 1.0 / x.e[2] } else { 0 },
|
||||
if x.e[3] != 0 { 1.0 / x.e[3] } else { 0 },
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// normalize returns a normalized form of the vector `x`
|
||||
pub fn (x Vec4) normalize() Vec4 {
|
||||
m := x.mod()
|
||||
if m == 0 {
|
||||
return zero_v4()
|
||||
}
|
||||
return Vec4{
|
||||
e: [
|
||||
x.e[0] * (1 / m),
|
||||
x.e[1] * (1 / m),
|
||||
x.e[2] * (1 / m),
|
||||
x.e[3] * (1 / m),
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// normalize3 returns a normalized form of the vector `x`, where the w element is set to 0
|
||||
pub fn (x Vec4) normalize3() Vec4 {
|
||||
m := x.mod3()
|
||||
if m == 0 {
|
||||
return zero_v4()
|
||||
}
|
||||
return Vec4{
|
||||
e: [
|
||||
x.e[0] * (1 / m),
|
||||
x.e[1] * (1 / m),
|
||||
x.e[2] * (1 / m),
|
||||
0,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// mod returns a module of the vector `x`
|
||||
@[inline]
|
||||
pub fn (x Vec4) mod() f32 {
|
||||
return math.sqrtf(x.e[0] * x.e[0] + x.e[1] * x.e[1] + x.e[2] * x.e[2] + x.e[3] * x.e[3])
|
||||
}
|
||||
|
||||
// mod3 returns a module of the 3d vector `x`, ignoring the value of its w element
|
||||
@[inline]
|
||||
pub fn (x Vec4) mod3() f32 {
|
||||
return math.sqrtf(x.e[0] * x.e[0] + x.e[1] * x.e[1] + x.e[2] * x.e[2])
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Math
|
||||
*********************************************************************/
|
||||
// zero_v4 returns a zero vector (all elements set to 0)
|
||||
@[inline]
|
||||
pub fn zero_v4() Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
f32(0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// one_v4 returns a vector, where all elements are set to 1
|
||||
@[inline]
|
||||
pub fn one_v4() Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
f32(1),
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// blank_v4 returns a vector, where all elements are set to 0, except `w`, which is set to 1
|
||||
pub fn blank_v4() Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
f32(0),
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// set_v4 returns a vector, where all elements are set to `value`
|
||||
@[inline]
|
||||
pub fn set_v4(value f32) Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
value,
|
||||
value,
|
||||
value,
|
||||
value,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// sum returns a sum of all the elements
|
||||
@[inline]
|
||||
pub fn (x Vec4) sum() f32 {
|
||||
return x.e[0] + x.e[1] + x.e[2] + x.e[3]
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Operators
|
||||
*********************************************************************/
|
||||
// + returns `a` + `b` (corresponding elements are added)
|
||||
@[inline]
|
||||
pub fn (a Vec4) + (b Vec4) Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
a.e[0] + b.e[0],
|
||||
a.e[1] + b.e[1],
|
||||
a.e[2] + b.e[2],
|
||||
a.e[3] + b.e[3],
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// - returns `a` + `b` (corresponding elements are subtracted)
|
||||
@[inline]
|
||||
pub fn (a Vec4) - (b Vec4) Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
a.e[0] - b.e[0],
|
||||
a.e[1] - b.e[1],
|
||||
a.e[2] - b.e[2],
|
||||
a.e[3] - b.e[3],
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// * returns `a` * `b` (corresponding elements are multiplied, then summed), i.e. a dot product
|
||||
@[inline]
|
||||
pub fn (a Vec4) * (b Vec4) f32 {
|
||||
return a.e[0] * b.e[0] + a.e[1] * b.e[1] + a.e[2] * b.e[2] + a.e[3] * b.e[3]
|
||||
}
|
||||
|
||||
// % returns a cross product of the vectors `a` and `b`
|
||||
@[inline]
|
||||
pub fn (a Vec4) % (b Vec4) Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
(a.e[1] * b.e[2]) - (a.e[2] * b.e[1]),
|
||||
(a.e[2] * b.e[0]) - (a.e[0] * b.e[2]),
|
||||
(a.e[0] * b.e[1]) - (a.e[1] * b.e[0]),
|
||||
0,
|
||||
]!
|
||||
}
|
||||
}
|
||||
|
||||
// mul_vec4 returns a vector, where the corresponding `x` and `y` elements are multiplied
|
||||
@[inline]
|
||||
pub fn (x Vec4) mul_vec4(y Vec4) Vec4 {
|
||||
return Vec4{
|
||||
e: [
|
||||
x.e[0] * y.e[0],
|
||||
x.e[1] * y.e[1],
|
||||
x.e[2] * y.e[2],
|
||||
x.e[3] * y.e[3],
|
||||
]!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
module gg
|
||||
|
||||
import encoding.base64
|
||||
import sokol.sapp
|
||||
import os
|
||||
|
||||
fn screenshot_stdout_payload(frame u64, png []u8) string {
|
||||
return '${gg_record_stdout_prefix} frame=${frame} format=png encoding=base64 data=${base64.encode(png)}'
|
||||
}
|
||||
|
||||
fn emit_recorded_frame_to_stdout(frame u64) ! {
|
||||
screenshot_file_path := os.join_path(os.vtmp_dir(),
|
||||
'vgg_record_stdout_${os.getpid()}_${frame}.png')
|
||||
sapp.screenshot_png(screenshot_file_path)!
|
||||
defer {
|
||||
os.rm(screenshot_file_path) or {}
|
||||
}
|
||||
png := os.read_bytes(screenshot_file_path)!
|
||||
println(screenshot_stdout_payload(frame, png))
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
// record_frame records the current frame to a file or stdout.
|
||||
// record_frame acts according to settings specified in `gg.recorder_settings`.
|
||||
@[if gg_record ?]
|
||||
pub fn (mut ctx Context) record_frame() {
|
||||
if ctx.frame in recorder_settings.screenshot_frames {
|
||||
match recorder_settings.screenshot_output {
|
||||
.stdout {
|
||||
$if gg_record_trace ? {
|
||||
eprintln('>>> screenshotting frame ${ctx.frame} to stdout')
|
||||
}
|
||||
emit_recorded_frame_to_stdout(ctx.frame) or { panic(err) }
|
||||
}
|
||||
.file {
|
||||
screenshot_file_path := '${recorder_settings.screenshot_prefix}${ctx.frame}.png'
|
||||
$if gg_record_trace ? {
|
||||
eprintln('>>> screenshotting ${screenshot_file_path}')
|
||||
}
|
||||
sapp.screenshot_png(screenshot_file_path) or { panic(err) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if ctx.frame == recorder_settings.stop_at_frame {
|
||||
$if gg_record_trace ? {
|
||||
eprintln('>>> exiting at frame ${ctx.frame}')
|
||||
}
|
||||
flush_stdout()
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_gg_recorder_settings() &SSRecorderSettings {
|
||||
$if gg_record ? {
|
||||
return new_gg_recorder_settings_from_env(os.environ(), os.executable())
|
||||
} $else {
|
||||
return &SSRecorderSettings{}
|
||||
}
|
||||
}
|
||||
|
||||
const recorder_settings = new_gg_recorder_settings()
|
||||
@@ -0,0 +1,4 @@
|
||||
module gg
|
||||
|
||||
@[if gg_record ?]
|
||||
pub fn (mut ctx Context) record_frame() {}
|
||||
@@ -0,0 +1,68 @@
|
||||
module gg
|
||||
|
||||
import os
|
||||
|
||||
enum ScreenshotOutput {
|
||||
file
|
||||
stdout
|
||||
}
|
||||
|
||||
const gg_record_stdout_prefix = '__V_GG_IMAGE__'
|
||||
|
||||
@[heap]
|
||||
pub struct SSRecorderSettings {
|
||||
pub mut:
|
||||
stop_at_frame i64 = -1
|
||||
screenshot_frames []u64
|
||||
screenshot_folder string
|
||||
screenshot_prefix string
|
||||
screenshot_output ScreenshotOutput = .file
|
||||
}
|
||||
|
||||
fn parse_screenshot_frames(value string) []u64 {
|
||||
mut frames := []u64{}
|
||||
for raw_frame in value.split(',') {
|
||||
frame := raw_frame.trim_space()
|
||||
if frame == '' {
|
||||
continue
|
||||
}
|
||||
frames << frame.u64()
|
||||
}
|
||||
return frames
|
||||
}
|
||||
|
||||
fn parse_screenshot_output(value string) ScreenshotOutput {
|
||||
return match value.trim_space().to_lower() {
|
||||
'stdout' { .stdout }
|
||||
else { .file }
|
||||
}
|
||||
}
|
||||
|
||||
fn recorder_env_value(env map[string]string, key string, fallback string) string {
|
||||
value := env[key]
|
||||
if value == '' {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fn new_gg_recorder_settings_from_env(env map[string]string, executable string) &SSRecorderSettings {
|
||||
mut stop_frame := recorder_env_value(env, 'VGG_STOP_AT_FRAME', '-1').i64()
|
||||
mut frames := parse_screenshot_frames(env['VGG_SCREENSHOT_FRAMES'])
|
||||
folder := env['VGG_SCREENSHOT_FOLDER']
|
||||
output := parse_screenshot_output(env['VGG_SCREENSHOT_OUTPUT'])
|
||||
if output == .stdout && frames.len == 0 {
|
||||
frames << u64(1)
|
||||
}
|
||||
if output == .stdout && stop_frame < 0 && frames.len > 0 {
|
||||
stop_frame = i64(frames[frames.len - 1])
|
||||
}
|
||||
prefix := os.join_path_single(folder, os.file_name(executable).all_before('.') + '_')
|
||||
return &SSRecorderSettings{
|
||||
stop_at_frame: stop_frame
|
||||
screenshot_frames: frames
|
||||
screenshot_folder: folder
|
||||
screenshot_prefix: prefix
|
||||
screenshot_output: output
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// vtest build: macos && !sanitized_job?
|
||||
import os
|
||||
import stbi
|
||||
|
||||
@[direct_array_access]
|
||||
fn png_has_non_black_pixels(path string) !bool {
|
||||
img := stbi.load(path)!
|
||||
defer {
|
||||
img.free()
|
||||
}
|
||||
for i in 0 .. img.width * img.height {
|
||||
unsafe {
|
||||
if img.data[i * 4] != 0 || img.data[i * 4 + 1] != 0 || img.data[i * 4 + 2] != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn test_streaming_r8_zero_buffer_stays_black_on_gl_backend() {
|
||||
vexe := @VEXE
|
||||
vroot := os.dir(vexe)
|
||||
sample_path := os.join_path(vroot, 'vlib/gg/testdata/streaming_r8_zero_buffer_issue_10989.vv')
|
||||
temp_dir := os.join_path(os.temp_dir(), 'v_gg_issue_10989_${os.getpid()}')
|
||||
os.mkdir_all(temp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(temp_dir) or {}
|
||||
}
|
||||
exe_path := os.join_path(temp_dir, 'issue10989_repro')
|
||||
// Force the GL backend so this exercises the packed texture upload path from issue #10989.
|
||||
compile_cmd := '${os.quoted_path(vexe)} -d gg_record -d darwin_sokol_glcore33 -o ${os.quoted_path(exe_path)} ${os.quoted_path(sample_path)}'
|
||||
compile_res := os.execute(compile_cmd)
|
||||
assert compile_res.exit_code == 0, compile_res.output
|
||||
run_cmd := 'VGG_STOP_AT_FRAME=2 VGG_SCREENSHOT_FRAMES=2 VGG_SCREENSHOT_FOLDER=${os.quoted_path(temp_dir)} ${os.quoted_path(exe_path)}'
|
||||
run_res := os.execute(run_cmd)
|
||||
if run_res.exit_code != 0 {
|
||||
if run_res.output.contains('glpixelformat') || run_res.exit_code == 134 {
|
||||
// OpenGL context creation failed (e.g. headless CI runners without GPU).
|
||||
// Skip the test rather than failing.
|
||||
eprintln('skipping: GL context creation failed on this system')
|
||||
return
|
||||
}
|
||||
assert run_res.exit_code == 0, run_res.output
|
||||
}
|
||||
screenshot_path := os.join_path(temp_dir, 'issue10989_repro_2.png')
|
||||
assert os.exists(screenshot_path)
|
||||
assert png_has_non_black_pixels(screenshot_path)! == false
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
module main
|
||||
|
||||
import math
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
window_title: 'Arcs'
|
||||
width: 200
|
||||
height: 200
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
ctx.draw_arc_filled(100, 100, 35, 45, 0, f32(math.radians(290)), 30, gg.red)
|
||||
ctx.draw_arc_empty(100, 100, 30, 50, 0, f32(math.radians(290)), 30, gg.white)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
width: 300
|
||||
height: 300
|
||||
window_title: 'Circles'
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
ctx.draw_circle_empty(150, 150, 80, gg.blue)
|
||||
ctx.draw_circle_filled(150, 150, 40, gg.yellow)
|
||||
ctx.draw_circle_line(150, 150, 80, 6, gg.red)
|
||||
ctx.draw_circle_line(150, 150, 120, 6, gg.green)
|
||||
ctx.draw_circle_line(150, 150, 150, 8, gg.white)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import gg
|
||||
|
||||
const thickness = f32(5)
|
||||
|
||||
gg.start(
|
||||
window_title: 'Ellipses'
|
||||
width: 600
|
||||
height: 400
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
drot := f32(ctx.frame) / 60
|
||||
ctx.draw_ellipse_filled(100, 100, 100, 50, gg.red)
|
||||
ctx.draw_ellipse_empty(100, 100, 50, 25, gg.black)
|
||||
ctx.draw_ellipse_filled_rotate(300, 100, 100, 50, -drot, gg.blue)
|
||||
ctx.draw_ellipse_empty_rotate(300, 100, 50, 25, -drot, gg.black)
|
||||
ctx.draw_ellipse_filled_rotate(500, 100, 100, 50, drot, gg.yellow)
|
||||
ctx.draw_ellipse_empty_rotate(500, 100, 50, 25, drot, gg.black)
|
||||
ctx.draw_ellipse_thick(100, 300, 100, 50, thickness, gg.green)
|
||||
ctx.draw_ellipse_thick_rotate(300, 300, 100, 50, thickness, drot, gg.purple)
|
||||
ctx.draw_ellipse_thick_rotate(500, 300, 100, 50, thickness, -drot, gg.indigo)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
bg_color: gg.rgb(174, 198, 255)
|
||||
width: 600
|
||||
height: 400
|
||||
window_title: 'Polygons'
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
ctx.draw_convex_poly([f32(100.0), 100.0, 200.0, 100.0, 300.0, 200.0, 200.0, 300.0, 100.0,
|
||||
300.0], gg.blue)
|
||||
ctx.draw_poly_empty([f32(50.0), 50.0, 70.0, 60.0, 90.0, 80.0, 70.0, 110.0], gg.black)
|
||||
ctx.draw_triangle_filled(450, 142, 530, 280, 370, 280, gg.red)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
bg_color: gg.white
|
||||
window_title: 'Rectangles'
|
||||
width: 250
|
||||
height: 100
|
||||
frame_fn: fn (ctx &gg.Context) {
|
||||
ctx.begin()
|
||||
ctx.draw_rect_filled(10, 10, 10, 10, gg.blue)
|
||||
ctx.draw_rect_empty(30, 10, 10, 10, gg.red)
|
||||
ctx.draw_rect_filled(50, 10, 30, 10, gg.green)
|
||||
ctx.draw_rect_empty(100, 10, 30, 10, gg.black)
|
||||
|
||||
ctx.draw_rect_empty(10, 50, 10, 10, gg.blue)
|
||||
ctx.draw_rect_filled(30, 50, 10, 10, gg.red)
|
||||
ctx.draw_rect_empty(50, 50, 30, 10, gg.green)
|
||||
ctx.draw_rect_filled(100, 50, 30, 10, gg.black)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
width: 325
|
||||
height: 275
|
||||
window_title: 'Rounded Rectangles'
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
// these should be rounded rectangles
|
||||
ctx.draw_rounded_rect_empty(10, 10, 50, 100, 5, gg.blue)
|
||||
ctx.draw_rounded_rect_empty(25, 25, 50, 100, 15, gg.yellow)
|
||||
ctx.draw_rounded_rect_empty(50, 50, 50, 100, 50, gg.red)
|
||||
ctx.draw_rounded_rect_empty(75, 75, 50, 100, 100, gg.green)
|
||||
ctx.draw_rounded_rect_empty(100, 100, 50, 100, 1000, gg.white)
|
||||
ctx.draw_rounded_rect_empty(110, 10, 100, 50, 5, gg.blue)
|
||||
ctx.draw_rounded_rect_empty(125, 25, 100, 50, 15, gg.yellow)
|
||||
ctx.draw_rounded_rect_empty(150, 50, 100, 50, 50, gg.red)
|
||||
ctx.draw_rounded_rect_empty(175, 75, 100, 50, 100, gg.green)
|
||||
ctx.draw_rounded_rect_empty(200, 100, 100, 50, 1000, gg.white)
|
||||
// this should be a perfect circle
|
||||
ctx.draw_rounded_rect_empty(10, 200, 50, 50, 1000, gg.magenta)
|
||||
// this should be a perfect square
|
||||
ctx.draw_rounded_rect_empty(250, 200, 50, 50, 0, gg.cyan)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
width: 325
|
||||
height: 275
|
||||
window_title: 'Rounded Rectangles'
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
// these should be rounded rectangles
|
||||
ctx.draw_rounded_rect_filled(10, 10, 50, 100, 5, gg.blue)
|
||||
ctx.draw_rounded_rect_filled(25, 25, 50, 100, 15, gg.yellow)
|
||||
ctx.draw_rounded_rect_filled(50, 50, 50, 100, 50, gg.red)
|
||||
ctx.draw_rounded_rect_filled(75, 75, 50, 100, 100, gg.green)
|
||||
ctx.draw_rounded_rect_filled(100, 100, 50, 100, 1000, gg.white)
|
||||
ctx.draw_rounded_rect_filled(110, 10, 100, 50, 5, gg.blue)
|
||||
ctx.draw_rounded_rect_filled(125, 25, 100, 50, 15, gg.yellow)
|
||||
ctx.draw_rounded_rect_filled(150, 50, 100, 50, 50, gg.red)
|
||||
ctx.draw_rounded_rect_filled(175, 75, 100, 50, 100, gg.green)
|
||||
ctx.draw_rounded_rect_filled(200, 100, 100, 50, 1000, gg.white)
|
||||
// this should be a perfect circle
|
||||
ctx.draw_rounded_rect_filled(10, 200, 50, 50, 1000, gg.magenta)
|
||||
// this should be a perfect square
|
||||
ctx.draw_rounded_rect_filled(250, 200, 50, 50, 0, gg.cyan)
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
gg &gg.Context = unsafe { nil }
|
||||
rotation f32 = f32(0)
|
||||
edge int = 3
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println('rotation: left arrow key, right arrow key')
|
||||
println('center polygon edge: up arrow key, down arrow key')
|
||||
|
||||
mut app := &App{}
|
||||
|
||||
app.gg = gg.new_context(
|
||||
window_title: 'Simple Polygons'
|
||||
width: 500
|
||||
height: 500
|
||||
bg_color: gg.black
|
||||
event_fn: event
|
||||
frame_fn: render
|
||||
user_data: app
|
||||
)
|
||||
|
||||
app.gg.run()
|
||||
}
|
||||
|
||||
fn render(app &App) {
|
||||
app.gg.begin()
|
||||
|
||||
color := gg.Color{
|
||||
r: 175
|
||||
g: 0
|
||||
b: 0
|
||||
a: 200
|
||||
}
|
||||
mut edge := 3
|
||||
for i := 0; i <= 5; i++ {
|
||||
for j := 0; j <= 5; j++ {
|
||||
if i == 0 || j == 0 || i == 5 || j == 5 {
|
||||
app.gg.draw_polygon_filled(50 + 80 * i, 50 + 80 * j, 30, edge, app.rotation,
|
||||
color)
|
||||
edge++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.gg.draw_polygon_filled(250, 250, 150, app.edge, app.rotation, color)
|
||||
|
||||
app.gg.end()
|
||||
}
|
||||
|
||||
fn event(e &gg.Event, mut app App) {
|
||||
match e.typ {
|
||||
.key_down {
|
||||
match e.key_code {
|
||||
.up {
|
||||
app.edge++
|
||||
}
|
||||
.down {
|
||||
if app.edge > 3 {
|
||||
app.edge--
|
||||
}
|
||||
}
|
||||
.right {
|
||||
app.rotation++
|
||||
}
|
||||
.left {
|
||||
app.rotation--
|
||||
}
|
||||
.escape {
|
||||
app.gg.quit()
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
gg.start(
|
||||
width: 640
|
||||
height: 300
|
||||
window_title: 'Circles'
|
||||
frame_fn: fn (mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
ctx.draw_text(10, 10, 'This text is at x: 10, y: 10', gg.TextCfg{ color: gg.white, size: 20 })
|
||||
ctx.draw_text(10, 40, 'This text is at x: 10, y: 40 and bold', gg.TextCfg{
|
||||
color: gg.white
|
||||
bold: true
|
||||
size: 20
|
||||
})
|
||||
ctx.draw_text(10, 70, 'This text is at x: 10, y: 70 and italic', gg.TextCfg{
|
||||
color: gg.white
|
||||
size: 20
|
||||
italic: true
|
||||
})
|
||||
ctx.draw_text(10, 100, 'This text is at x: 10, y: 110 and mono', gg.TextCfg{
|
||||
color: gg.white
|
||||
size: 20
|
||||
mono: true
|
||||
})
|
||||
ctx.draw_text(10, 130, 'This text is at x: 10, y: 130 and twice as big', gg.TextCfg{
|
||||
color: gg.white
|
||||
size: 40
|
||||
})
|
||||
ctx.draw_text(10, 180, 'This text is at x: 10, y: 180 and red', gg.TextCfg{
|
||||
color: gg.red
|
||||
size: 20
|
||||
})
|
||||
ctx.draw_text(10, 210, 'This text is at x: 10, y: 210 and blue', gg.TextCfg{
|
||||
color: gg.blue
|
||||
size: 20
|
||||
})
|
||||
ctx.draw_text(10, 240, 'This text is at x: 10, y: 240 and green', gg.TextCfg{
|
||||
color: gg.green
|
||||
size: 20
|
||||
})
|
||||
ctx.end()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
import sokol.gfx
|
||||
|
||||
fn main() {
|
||||
mut context := gg.new_context(
|
||||
frame_fn: frame
|
||||
width: 500
|
||||
height: 500
|
||||
)
|
||||
context.run()
|
||||
}
|
||||
|
||||
fn frame(mut ctx gg.Context) {
|
||||
ctx.begin()
|
||||
id := ctx.new_streaming_image(ctx.width, ctx.height, 4, pixel_format: .rgba8)
|
||||
mut img := ctx.get_cached_image_by_idx(id)
|
||||
mut bytes := []u8{len: img.width * img.height * 4, cap: img.width * img.height * 4}
|
||||
for y in 0 .. img.height {
|
||||
for x in 0 .. img.width {
|
||||
unsafe {
|
||||
bytes[(x + img.width * y) * 4] = 100
|
||||
bytes[(x + img.width * y) * 4 + 1] = 100
|
||||
bytes[(x + img.width * y) * 4 + 2] = 100
|
||||
bytes[(x + img.width * y) * 4 + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
img.update_pixel_data(&bytes[0])
|
||||
}
|
||||
ctx.draw_image(0, 0, ctx.width, ctx.height, img)
|
||||
ctx.remove_cached_image_by_idx(id)
|
||||
ctx.end()
|
||||
gfx.destroy_image(img.simg)
|
||||
unsafe {
|
||||
free(&bytes[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
const win_width = 100
|
||||
const win_height = 100
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
gg &gg.Context = unsafe { nil }
|
||||
image int
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.gg = gg.new_context(
|
||||
width: win_width
|
||||
height: win_height
|
||||
window_title: 'issue 10989'
|
||||
frame_fn: frame
|
||||
user_data: app
|
||||
init_fn: initialize
|
||||
)
|
||||
app.gg.run()
|
||||
}
|
||||
|
||||
fn initialize(mut app App) {
|
||||
buffer := []u8{len: 10 * 10}
|
||||
app.image = app.gg.new_streaming_image(10, 10, 1, pixel_format: .r8)
|
||||
app.gg.update_pixel_data(app.image, buffer.data)
|
||||
}
|
||||
|
||||
fn frame(mut app App) {
|
||||
app.gg.begin()
|
||||
app.gg.draw_image_by_id(0, 0, 10, 10, app.image)
|
||||
app.gg.end()
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
module main
|
||||
|
||||
import gg
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
gg &gg.Context = unsafe { nil }
|
||||
radius f64 = 10.0
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.gg = gg.new_context(
|
||||
bg_color: gg.white
|
||||
width: 300
|
||||
height: 300
|
||||
window_title: 'Circles'
|
||||
frame_fn: frame
|
||||
event_fn: on_event
|
||||
user_data: app
|
||||
)
|
||||
app.gg.run()
|
||||
}
|
||||
|
||||
fn on_event(e &gg.Event, mut app App) {
|
||||
match e.typ {
|
||||
.key_down {
|
||||
match e.key_code {
|
||||
.up {
|
||||
app.radius += 1
|
||||
}
|
||||
.down {
|
||||
app.radius -= 1
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(mut app App) {
|
||||
app.gg.begin()
|
||||
app.gg.draw_circle_empty(150, 150, f32(app.radius), gg.blue)
|
||||
app.gg.draw_text(20, 20, 'radius: ${app.radius}')
|
||||
// app.gg.draw_text(20, 50, 'circle_segment_size: ${circle_segment_size}')
|
||||
app.gg.end()
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module gg
|
||||
|
||||
import fontstash
|
||||
import sokol.sfons
|
||||
import sokol.sgl
|
||||
import os
|
||||
import os.font
|
||||
|
||||
pub struct FT {
|
||||
pub:
|
||||
fons &fontstash.Context = unsafe { nil }
|
||||
font_normal int
|
||||
font_bold int
|
||||
font_mono int
|
||||
font_italic int
|
||||
pub mut:
|
||||
fonts_map map[string]int // for storing custom fonts, provided via cfg.family in draw_text()
|
||||
scale f32 = 1.0
|
||||
}
|
||||
|
||||
pub enum HorizontalAlign {
|
||||
left = C.FONS_ALIGN_LEFT
|
||||
center = C.FONS_ALIGN_CENTER
|
||||
right = C.FONS_ALIGN_RIGHT
|
||||
}
|
||||
|
||||
pub enum VerticalAlign {
|
||||
top = C.FONS_ALIGN_TOP
|
||||
middle = C.FONS_ALIGN_MIDDLE
|
||||
bottom = C.FONS_ALIGN_BOTTOM
|
||||
baseline = C.FONS_ALIGN_BASELINE
|
||||
}
|
||||
|
||||
const initial_text_atlas_size = int($d('gg_text_buff_size', 2048))
|
||||
const max_text_atlas_size = 8192
|
||||
|
||||
fn expand_atlas_callback(uptr voidptr, error int, _val int) {
|
||||
if error != C.FONS_ATLAS_FULL {
|
||||
return
|
||||
}
|
||||
fons := unsafe { &fontstash.Context(uptr) }
|
||||
width, height := fons.get_atlas_size()
|
||||
mut next_width := if width > 0 { width } else { initial_text_atlas_size }
|
||||
mut next_height := if height > 0 { height } else { initial_text_atlas_size }
|
||||
if next_width < max_text_atlas_size {
|
||||
next_width = if next_width * 2 > max_text_atlas_size {
|
||||
max_text_atlas_size
|
||||
} else {
|
||||
next_width * 2
|
||||
}
|
||||
}
|
||||
if next_height < max_text_atlas_size {
|
||||
next_height = if next_height * 2 > max_text_atlas_size {
|
||||
max_text_atlas_size
|
||||
} else {
|
||||
next_height * 2
|
||||
}
|
||||
}
|
||||
if next_width == width && next_height == height {
|
||||
return
|
||||
}
|
||||
fons.expand_atlas(next_width, next_height)
|
||||
}
|
||||
|
||||
fn new_ft(c FTConfig) ?&FT {
|
||||
if c.font_path == '' {
|
||||
if c.bytes_normal.len > 0 {
|
||||
fons := sfons.create(initial_text_atlas_size, initial_text_atlas_size, 1)
|
||||
bytes_normal := c.bytes_normal
|
||||
bytes_bold := if c.bytes_bold.len > 0 {
|
||||
c.bytes_bold
|
||||
} else {
|
||||
debug_font_println('setting bold variant to normal')
|
||||
bytes_normal
|
||||
}
|
||||
bytes_mono := if c.bytes_mono.len > 0 {
|
||||
c.bytes_mono
|
||||
} else {
|
||||
debug_font_println('setting mono variant to normal')
|
||||
bytes_normal
|
||||
}
|
||||
bytes_italic := if c.bytes_italic.len > 0 {
|
||||
c.bytes_italic
|
||||
} else {
|
||||
debug_font_println('setting italic variant to normal')
|
||||
bytes_normal
|
||||
}
|
||||
fons.set_error_callback(expand_atlas_callback, fons)
|
||||
return &FT{
|
||||
fons: fons
|
||||
font_normal: fons.add_font_mem('sans', bytes_normal.clone(), true)
|
||||
font_bold: fons.add_font_mem('sans', bytes_bold.clone(), true)
|
||||
font_mono: fons.add_font_mem('sans', bytes_mono.clone(), true)
|
||||
font_italic: fons.add_font_mem('sans', bytes_italic.clone(), true)
|
||||
scale: c.scale
|
||||
}
|
||||
} else {
|
||||
// Load default font
|
||||
}
|
||||
}
|
||||
|
||||
if c.font_path == '' || !os.exists(c.font_path) {
|
||||
$if !android {
|
||||
println('failed to load font "${c.font_path}"')
|
||||
return none
|
||||
}
|
||||
}
|
||||
|
||||
mut normal_path := c.font_path
|
||||
mut bytes := []u8{}
|
||||
$if android {
|
||||
// First try any filesystem paths
|
||||
bytes = os.read_bytes(c.font_path) or { []u8{} }
|
||||
if bytes.len == 0 {
|
||||
// ... then try the APK asset path
|
||||
bytes = os.read_apk_asset(c.font_path) or {
|
||||
println('failed to load font "${c.font_path}"')
|
||||
return none
|
||||
}
|
||||
}
|
||||
} $else {
|
||||
bytes = os.read_bytes(c.font_path) or {
|
||||
println('failed to load font "${c.font_path}"')
|
||||
return none
|
||||
}
|
||||
}
|
||||
mut bold_path := if c.custom_bold_font_path != '' {
|
||||
c.custom_bold_font_path
|
||||
} else {
|
||||
font.get_path_variant(c.font_path, .bold)
|
||||
}
|
||||
bytes_bold := os.read_bytes(bold_path) or {
|
||||
debug_font_println('failed to load font "${bold_path}"')
|
||||
bold_path = c.font_path
|
||||
bytes
|
||||
}
|
||||
mut mono_path := font.get_path_variant(c.font_path, .mono)
|
||||
bytes_mono := os.read_bytes(mono_path) or {
|
||||
debug_font_println('failed to load font "${mono_path}"')
|
||||
mono_path = c.font_path
|
||||
bytes
|
||||
}
|
||||
mut italic_path := font.get_path_variant(c.font_path, .italic)
|
||||
bytes_italic := os.read_bytes(italic_path) or {
|
||||
debug_font_println('failed to load font "${italic_path}"')
|
||||
italic_path = c.font_path
|
||||
bytes
|
||||
}
|
||||
fons := sfons.create(initial_text_atlas_size, initial_text_atlas_size, 1)
|
||||
debug_font_println('Font used for font_normal : ${normal_path}')
|
||||
debug_font_println('Font used for font_bold : ${bold_path}')
|
||||
debug_font_println('Font used for font_mono : ${mono_path}')
|
||||
debug_font_println('Font used for font_italic : ${italic_path}')
|
||||
fons.set_error_callback(expand_atlas_callback, fons)
|
||||
return &FT{
|
||||
fons: fons
|
||||
font_normal: fons.add_font_mem('sans', bytes.clone(), true)
|
||||
font_bold: fons.add_font_mem('sans', bytes_bold.clone(), true)
|
||||
font_mono: fons.add_font_mem('sans', bytes_mono.clone(), true)
|
||||
font_italic: fons.add_font_mem('sans', bytes_italic.clone(), true)
|
||||
scale: c.scale
|
||||
}
|
||||
}
|
||||
|
||||
// set_text_cfg sets the current text configuration
|
||||
pub fn (ctx &Context) set_text_cfg(cfg TextCfg) {
|
||||
if !ctx.font_inited {
|
||||
return
|
||||
}
|
||||
if cfg.family != '' {
|
||||
// println('set text cfg family=${cfg.family}')
|
||||
mut f := ctx.ft.fonts_map[cfg.family]
|
||||
if f == 0 {
|
||||
// No such font in the cache yet, create it
|
||||
bytes := os.read_bytes(cfg.family) or {
|
||||
debug_font_println('failed to load font "${cfg.family}"')
|
||||
return
|
||||
}
|
||||
f = ctx.ft.fons.add_font_mem(cfg.family, bytes.clone(), true)
|
||||
unsafe {
|
||||
ctx.ft.fonts_map[cfg.family] = f
|
||||
}
|
||||
}
|
||||
ctx.ft.fons.set_font(f)
|
||||
} else if cfg.bold {
|
||||
ctx.ft.fons.set_font(ctx.ft.font_bold)
|
||||
} else if cfg.mono {
|
||||
ctx.ft.fons.set_font(ctx.ft.font_mono)
|
||||
} else if cfg.italic {
|
||||
ctx.ft.fons.set_font(ctx.ft.font_italic)
|
||||
} else {
|
||||
ctx.ft.fons.set_font(ctx.ft.font_normal)
|
||||
}
|
||||
scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
|
||||
size := if cfg.mono { cfg.size - 2 } else { cfg.size }
|
||||
ctx.ft.fons.set_size(scale * f32(size))
|
||||
ctx.ft.fons.set_align(int(cfg.align) | int(cfg.vertical_align))
|
||||
color := sfons.rgba(cfg.color.r, cfg.color.g, cfg.color.b, cfg.color.a)
|
||||
if cfg.color.a != 255 {
|
||||
sgl.load_pipeline(ctx.pipeline.alpha)
|
||||
}
|
||||
ctx.ft.fons.set_color(color)
|
||||
ascender := f32(0.0)
|
||||
descender := f32(0.0)
|
||||
lh := f32(0.0)
|
||||
ctx.ft.fons.vert_metrics(&ascender, &descender, &lh)
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct DrawTextParams {
|
||||
pub:
|
||||
x int
|
||||
y int
|
||||
text string
|
||||
|
||||
color Color = black
|
||||
size int = 16
|
||||
align HorizontalAlign = .left
|
||||
vertical_align VerticalAlign = .top
|
||||
max_width int
|
||||
family string
|
||||
bold bool
|
||||
mono bool
|
||||
italic bool
|
||||
}
|
||||
|
||||
pub fn (ctx &Context) draw_text2(p DrawTextParams) {
|
||||
ctx.draw_text(p.x, p.y, p.text, TextCfg{
|
||||
color: p.color
|
||||
size: p.size
|
||||
align: p.align
|
||||
vertical_align: p.vertical_align
|
||||
max_width: p.max_width
|
||||
family: p.family
|
||||
bold: p.bold
|
||||
mono: p.mono
|
||||
italic: p.italic
|
||||
}) // TODO: perf once it's the only function to draw text
|
||||
}
|
||||
|
||||
// draw_text draws the string in `text_` starting at top-left position `x`,`y`.
|
||||
// Text settings can be provided with `cfg`.
|
||||
pub fn (ctx &Context) draw_text(x int, y int, text_ string, cfg TextCfg) {
|
||||
$if macos {
|
||||
if ctx.native_rendering {
|
||||
if cfg.align == align_right {
|
||||
width := ctx.text_width(text_)
|
||||
// println('draw text ctx.height = ${ctx.height}')
|
||||
C.darwin_draw_string(x - width, ctx.height - y, text_, cfg)
|
||||
} else {
|
||||
C.darwin_draw_string(x, ctx.height - y, text_, cfg)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if !ctx.font_inited {
|
||||
eprintln('gg: draw_text(): font not initialized')
|
||||
return
|
||||
}
|
||||
// text := text_.trim_space() // TODO: remove/optimize
|
||||
// mut text := text_
|
||||
// if text.contains('\t') {
|
||||
// text = text.replace('\t', ' ')
|
||||
// }
|
||||
ctx.set_text_cfg(cfg)
|
||||
scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
|
||||
ctx.ft.fons.draw_text(x * scale, y * scale, text_) // TODO: check offsets/alignment
|
||||
}
|
||||
|
||||
// draw_text draws the string in `text_` starting at top-left position `x`,`y` using
|
||||
// default text settings.
|
||||
pub fn (ctx &Context) draw_text_def(x int, y int, text string) {
|
||||
ctx.draw_text(x, y, text)
|
||||
}
|
||||
|
||||
// flush prepares the font for use.
|
||||
pub fn (ft &FT) flush() {
|
||||
sfons.flush(ft.fons)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (ctx &Context) text_metrics(s string) (f32, [4]f32) {
|
||||
mut bounds := [4]f32{}
|
||||
advance := ctx.ft.fons.text_bounds(0, 0, s, &bounds[0])
|
||||
return advance, bounds
|
||||
}
|
||||
|
||||
// text_width returns the width of the `string` `s` in pixels.
|
||||
pub fn (ctx &Context) text_width(s string) int {
|
||||
$if macos {
|
||||
if ctx.native_rendering {
|
||||
return C.darwin_text_width(s)
|
||||
}
|
||||
}
|
||||
// ctx.set_text_cfg(cfg) TODO
|
||||
if !ctx.font_inited {
|
||||
return 0
|
||||
}
|
||||
advance, _ := ctx.text_metrics(s)
|
||||
return int(advance / ctx.scale)
|
||||
}
|
||||
|
||||
// text_height returns the height of the `string` `s` in pixels.
|
||||
pub fn (ctx &Context) text_height(s string) int {
|
||||
// ctx.set_text_cfg(cfg) TODO
|
||||
if !ctx.font_inited {
|
||||
return 0
|
||||
}
|
||||
_, bounds := ctx.text_metrics(s)
|
||||
return int((bounds[3] - bounds[1]) / ctx.scale)
|
||||
}
|
||||
|
||||
// text_size returns the width and height of the `string` `s` in pixels.
|
||||
pub fn (ctx &Context) text_size(s string) (int, int) {
|
||||
// ctx.set_text_cfg(cfg) TODO
|
||||
if !ctx.font_inited {
|
||||
return 0, 0
|
||||
}
|
||||
advance, bounds := ctx.text_metrics(s)
|
||||
return int(advance / ctx.scale), int((bounds[3] - bounds[1]) / ctx.scale)
|
||||
}
|
||||
|
||||
// text_width returns the width of the `string` `s` in pixels.
|
||||
pub fn (ctx &Context) text_width_f(s string) f32 {
|
||||
$if macos {
|
||||
if ctx.native_rendering {
|
||||
return C.darwin_text_width(s)
|
||||
}
|
||||
}
|
||||
// ctx.set_text_cfg(cfg) TODO
|
||||
if !ctx.font_inited {
|
||||
return 0
|
||||
}
|
||||
advance, _ := ctx.text_metrics(s)
|
||||
return advance / ctx.scale
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
module gg
|
||||
|
||||
pub enum HorizontalAlign {
|
||||
left
|
||||
center
|
||||
right
|
||||
}
|
||||
|
||||
pub enum VerticalAlign {
|
||||
top
|
||||
middle
|
||||
bottom
|
||||
baseline
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) draw_text(x int, y int, text_ string, cfg TextCfg) {
|
||||
ctx.context.fillStyle = cfg.color.to_css_string().str
|
||||
ctx.context.font = cfg.to_css_string().str
|
||||
ctx.context.fillText(text_.str, JS.Number(x), JS.Number(y))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
||||
module gg
|
||||
|
||||
pub const align_right = HorizontalAlign.right
|
||||
pub const align_left = HorizontalAlign.left
|
||||
|
||||
struct FTConfig {
|
||||
font_path string
|
||||
custom_bold_font_path string
|
||||
scale f32 = 1.0
|
||||
font_size int
|
||||
bytes_normal []u8
|
||||
bytes_bold []u8
|
||||
bytes_mono []u8
|
||||
bytes_italic []u8
|
||||
}
|
||||
|
||||
struct StringToRender {
|
||||
x int
|
||||
y int
|
||||
text string
|
||||
cfg TextCfg
|
||||
}
|
||||
|
||||
@[if debug_font ?]
|
||||
fn debug_font_println(s string) {
|
||||
println(s)
|
||||
}
|
||||
|
||||
@[markused; params]
|
||||
pub struct TextCfg {
|
||||
pub:
|
||||
color Color = black
|
||||
size int = 16
|
||||
align HorizontalAlign = .left
|
||||
vertical_align VerticalAlign = .top
|
||||
max_width int
|
||||
family string
|
||||
bold bool
|
||||
mono bool
|
||||
italic bool
|
||||
}
|
||||
|
||||
// to_css_string returns a CSS compatible string of the TextCfg `cfg`.
|
||||
// For example: `'mono 14px serif'`.
|
||||
pub fn (cfg &TextCfg) to_css_string() string {
|
||||
mut font_style := ''
|
||||
if cfg.bold {
|
||||
font_style += 'bold '
|
||||
}
|
||||
if cfg.mono {
|
||||
font_style += 'mono '
|
||||
}
|
||||
if cfg.italic {
|
||||
font_style += 'italic '
|
||||
}
|
||||
return '${font_style} ${cfg.size}px ${cfg.family}'
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// vtest build: !msvc
|
||||
// vtest flaky: true
|
||||
// vtest retry: 1
|
||||
module gg
|
||||
|
||||
import fontstash
|
||||
|
||||
const embedded_test_mono_font = $embed_file('@VEXEROOT/examples/assets/fonts/RobotoMono-Regular.ttf')
|
||||
|
||||
fn new_test_text_context() (&Context, &fontstash.Context) {
|
||||
return new_test_text_context_with_atlas(512, 512)
|
||||
}
|
||||
|
||||
fn new_test_text_context_with_atlas(width int, height int) (&Context, &fontstash.Context) {
|
||||
mut fons := fontstash.create_internal(&C.FONSparams{
|
||||
width: width
|
||||
height: height
|
||||
flags: 0
|
||||
})
|
||||
assert fons != unsafe { nil }
|
||||
font_id := fons.add_font_mem('mono', embedded_test_mono_font.to_bytes().clone(), true)
|
||||
assert font_id != fontstash.invalid
|
||||
fons.set_font(font_id)
|
||||
fons.set_size(16)
|
||||
fons.set_error_callback(expand_atlas_callback, fons)
|
||||
return &Context{
|
||||
ft: &FT{
|
||||
fons: fons
|
||||
}
|
||||
font_inited: true
|
||||
scale: 1.0
|
||||
}, fons
|
||||
}
|
||||
|
||||
fn text_advance_and_bounds_width(fons &fontstash.Context, text string) (f32, f32) {
|
||||
mut bounds := [4]f32{}
|
||||
advance := fons.text_bounds(0, 0, text, &bounds[0])
|
||||
return advance, bounds[2] - bounds[0]
|
||||
}
|
||||
|
||||
fn test_text_width_uses_font_advance_for_monospaced_fonts() {
|
||||
ctx, fons := new_test_text_context()
|
||||
defer {
|
||||
fontstash.delete_internal(fons)
|
||||
}
|
||||
i_advance, i_bounds_width := text_advance_and_bounds_width(fons, 'i')
|
||||
m_advance, m_bounds_width := text_advance_and_bounds_width(fons, 'm')
|
||||
assert int(i_advance / ctx.scale) == int(m_advance / ctx.scale)
|
||||
assert int(i_bounds_width / ctx.scale) != int(m_bounds_width / ctx.scale)
|
||||
assert ctx.text_width('i') == int(i_advance / ctx.scale)
|
||||
assert ctx.text_width('m') == int(m_advance / ctx.scale)
|
||||
width, height := ctx.text_size('i')
|
||||
assert width == ctx.text_width('i')
|
||||
assert height > 0
|
||||
}
|
||||
|
||||
fn test_text_width_counts_trailing_space_advance() {
|
||||
ctx, fons := new_test_text_context()
|
||||
defer {
|
||||
fontstash.delete_internal(fons)
|
||||
}
|
||||
advance, bounds_width := text_advance_and_bounds_width(fons, 'a ')
|
||||
assert int(advance / ctx.scale) > int(bounds_width / ctx.scale)
|
||||
assert ctx.text_width('a ') == int(advance / ctx.scale)
|
||||
width, _ := ctx.text_size('a ')
|
||||
assert width == int(advance / ctx.scale)
|
||||
}
|
||||
|
||||
fn test_text_atlas_expands_on_overflow() {
|
||||
_, fons := new_test_text_context_with_atlas(64, 64)
|
||||
defer {
|
||||
fontstash.delete_internal(fons)
|
||||
}
|
||||
before_width, before_height := fons.get_atlas_size()
|
||||
mut bounds := [4]f32{}
|
||||
for size in [48, 64, 80] {
|
||||
fons.set_size(size)
|
||||
_ = fons.text_bounds(0, 0, 'Some very long text here 0123456789', &bounds[0])
|
||||
}
|
||||
after_width, after_height := fons.get_atlas_size()
|
||||
assert after_width > before_width || after_height > before_height
|
||||
}
|
||||
Reference in new issue
Block a user