├── .github └── workflows │ └── go.yml ├── .gitignore ├── ANDROID.md ├── CODE_OF_CONDUCT.md ├── COMPATIBILITY.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── alg ├── degrees.go ├── degrees_test.go ├── doc.go ├── floatgeom │ ├── dir.go │ ├── dir_test.go │ ├── doc.go │ ├── point.go │ ├── point_test.go │ ├── polygon.go │ ├── polygon_test.go │ ├── rect.go │ ├── rect_test.go │ ├── triangle.go │ └── triangle_test.go ├── intgeom │ ├── dir.go │ ├── dir_test.go │ ├── doc.go │ ├── point.go │ ├── point_test.go │ ├── rect.go │ └── rect_test.go ├── math.go ├── math_test.go ├── selection.go ├── selection_test.go ├── span │ ├── builtin.go │ ├── builtin_test.go │ ├── color.go │ ├── color_test.go │ ├── doc.go │ ├── internal │ │ └── random │ │ │ └── rand.go │ └── span.go ├── stwHeap.go ├── triangulate.go └── triangulate_test.go ├── audio ├── driver.go ├── driver_test.go ├── fade.go ├── file_cache.go ├── file_load.go ├── format │ ├── ceol │ │ ├── ceol.go │ │ └── testdata │ │ │ └── test.ceol │ ├── dls │ │ ├── dls.go │ │ └── testdata │ │ │ └── SanbikiSCC.dls │ ├── flac │ │ └── flac.go │ ├── mp3 │ │ └── mp3.go │ ├── register.go │ ├── riff │ │ ├── info.go │ │ └── riff.go │ └── wav │ │ ├── testdata │ │ └── test.wav │ │ └── wav.go ├── init_darwin.go ├── init_linux.go ├── internal │ └── dsound │ │ └── dsound.go ├── pcm │ └── interface.go ├── play.go ├── play_test.go ├── reader.go ├── synth │ ├── filter_test.go │ ├── option.go │ ├── pitch.go │ ├── source.go │ └── waves.go ├── testdata │ └── test.wav ├── writer.go ├── writer_alsa.go ├── writer_dsound.go ├── writer_other.go └── writer_pulse.go ├── collision ├── attachSpace.go ├── attachSpace_test.go ├── default.go ├── default_test.go ├── doc.go ├── filter.go ├── geom.go ├── label.go ├── onCollision.go ├── onCollision_test.go ├── onHit.go ├── point.go ├── point_test.go ├── ray │ ├── castFilter.go │ ├── castLimit.go │ ├── caster.go │ ├── caster_test.go │ ├── coneCaster.go │ ├── coneCaster_test.go │ ├── doc.go │ └── raycast_test.go ├── reactiveSpace.go ├── reactiveSpace_test.go ├── rtree-LICENSE ├── rtree.go ├── rtree_test.go ├── space.go ├── space_test.go ├── tree.go └── tree_test.go ├── config.go ├── config_test.go ├── debugstream ├── commands.go ├── commands_test.go ├── defaultcommands.go ├── doc.go ├── mispellDetector.go ├── mispellDetector_test.go ├── scopeHelper.go └── scopeHelper_test.go ├── debugtools ├── doc.go ├── inputviz │ ├── controllerOutline.png │ ├── joystick.go │ ├── keyboard.go │ └── mouse.go ├── mouse.go ├── renderable.go └── tree.go ├── default.go ├── default_desktop.go ├── default_test.go ├── dlog ├── default.go ├── default_test.go ├── dlog.go ├── dlog_test.go ├── doc.go ├── levels.go ├── levels_test.go └── strings.go ├── drawLoop.go ├── driver.go ├── entities ├── doc.go ├── entity.go ├── move.go ├── opts_gen.go └── x │ ├── btn │ ├── button.go │ ├── grid │ │ ├── grid.go │ │ └── option.go │ ├── option.go │ ├── textOptions.go │ └── tree.go │ └── doc.go ├── event ├── bind.go ├── bind_test.go ├── bus.go ├── bus_test.go ├── caller.go ├── caller_test.go ├── default.go ├── doc.go ├── events.go ├── handler.go ├── internal.go ├── response.go ├── response_test.go ├── trigger.go └── trigger_test.go ├── examples ├── README.md ├── adventure │ ├── example.PNG │ └── main.go ├── bezier │ ├── README.md │ ├── example.PNG │ └── main.go ├── blank │ ├── README.md │ └── main.go ├── click-propagation │ └── main.go ├── collision │ ├── README.md │ └── main.go ├── flappy-bird │ ├── README.md │ ├── example.gif │ └── main.go ├── joystick-viz │ ├── README.md │ ├── controllerOutline.png │ ├── example.gif │ └── main.go ├── js-server │ ├── README.md │ ├── index.html.tpl │ ├── main.go │ └── wasm_exec.js ├── keyboard-viz │ └── main.go ├── mouse-viz │ └── main.go ├── multi-window │ ├── README.md │ ├── example.PNG │ └── main.go ├── particle-demo │ ├── README.md │ ├── main.go │ └── overviewExample.gif ├── piano │ ├── example.gif │ └── main.go ├── platformer │ ├── example.gif │ └── main.go ├── pong │ ├── README.md │ └── main.go ├── rooms │ ├── example.PNG │ └── main.go ├── scenes │ └── main.go ├── screenopts │ ├── example.PNG │ └── main.go ├── sprite │ ├── README.md │ ├── assets │ │ └── images │ │ │ └── raw │ │ │ └── gopher11.png │ └── main.go ├── text │ ├── README.md │ ├── assets │ │ └── font │ │ │ └── luxisbi.ttf │ ├── go.mod │ ├── go.sum │ └── main.go └── top-down-shooter │ ├── assets │ └── images │ │ ├── 16x16 │ │ └── sheet.png │ │ └── character │ │ └── eggplant-fish.png │ ├── example.gif │ └── main.go ├── fileutil ├── doc.go ├── open.go ├── open_test.go ├── os_fallback_js.go └── testdata │ └── test.txt ├── go.mod ├── go.sum ├── init.go ├── init_override_js.go ├── init_override_other.go ├── init_test.go ├── inputLoop.go ├── inputLoop_test.go ├── inputTracker.go ├── inputTracker_test.go ├── joystick ├── driver_darwin.go ├── driver_js.go ├── driver_linux.go ├── driver_other.go ├── driver_windows.go └── joystick.go ├── key ├── doc.go ├── events.go ├── keycodes.go ├── keys.go ├── state.go └── state_test.go ├── lifecycle.go ├── lifecycle_test.go ├── loading.go ├── loading_test.go ├── mouse ├── default.go ├── default_test.go ├── doc.go ├── event.go ├── event_test.go ├── events.go ├── events_test.go ├── mouse.go ├── mouse_test.go ├── onCollision.go └── onCollision_test.go ├── oakerr ├── doc.go ├── errors.go ├── errors_test.go ├── format_string.go ├── language.go └── language_test.go ├── physics ├── attach.go ├── attach_test.go ├── doc.go ├── force.go ├── force_test.go ├── vector.go └── vector_test.go ├── render ├── bachload_test.go ├── batchload.go ├── bezier.go ├── bezier_test.go ├── cache.go ├── cache_test.go ├── colorbox.go ├── colorbox_test.go ├── colorer.go ├── colorer_test.go ├── compositeM.go ├── compositeM_test.go ├── compositeR.go ├── compositeR_test.go ├── curve.go ├── curve_test.go ├── decoder.go ├── decoder_test.go ├── default_decoders.go ├── default_font.go ├── default_font_test.go ├── doc.go ├── draw.go ├── drawHeap.go ├── drawHeap_test.go ├── drawStack.go ├── drawStack_test.go ├── draw_test.go ├── font.go ├── font_test.go ├── fps.go ├── fps_test.go ├── gradientbox.go ├── gradients.go ├── gradients_test.go ├── interfaceFeatures.go ├── interruptable.go ├── layered.go ├── layered_test.go ├── line.go ├── line_test.go ├── loadsheet.go ├── loadsheet_test.go ├── loadsprite.go ├── loadsprite_test.go ├── logicfps.go ├── logicfps_test.go ├── luxisr.ttf ├── mod │ ├── cut.go │ ├── doc.go │ ├── filter.go │ ├── gift.go │ ├── highlight.go │ ├── mod.go │ └── mod_test.go ├── modifiable.go ├── noopStackable.go ├── noopStackable_test.go ├── particle │ ├── allocator.go │ ├── allocator_test.go │ ├── collisionParticle.go │ ├── collision_test.go │ ├── collisonGenerator.go │ ├── color.go │ ├── colorGenerator.go │ ├── colorParticle.go │ ├── color_test.go │ ├── doc.go │ ├── generator.go │ ├── generator_test.go │ ├── gradientGenerator.go │ ├── gradientParticle.go │ ├── gradient_test.go │ ├── math.go │ ├── options.go │ ├── options_test.go │ ├── particle.go │ ├── particle_test.go │ ├── shape.go │ ├── source.go │ ├── source_test.go │ ├── spriteGenerator.go │ ├── spriteParticle.go │ └── sprite_test.go ├── pausing.go ├── polygon.go ├── polygon_test.go ├── renderable.go ├── reverting.go ├── reverting_test.go ├── sequence.go ├── sequence_test.go ├── sheet.go ├── sheet_test.go ├── shiny.go ├── shiny_test.go ├── sprite.go ├── sprite_test.go ├── switch.go ├── switch_test.go ├── testdata │ └── assets │ │ ├── fonts │ │ ├── luxisr.ttf │ │ └── seguiemj.ttf │ │ └── images │ │ ├── 16x16 │ │ ├── bad.png │ │ ├── baddims0x0.png │ │ └── jeremy.png │ │ ├── devfile.pdn │ │ ├── eyes3x3.png │ │ └── raw │ │ └── nonsheet.png ├── text.go ├── text_test.go ├── tween.go └── tween_test.go ├── scene.go ├── scene ├── README.md ├── context.go ├── context_desktop.go ├── context_other.go ├── delay.go ├── delay_test.go ├── doc.go ├── example_test.go ├── map.go ├── map_test.go ├── scene.go ├── scene_test.go ├── transition.go └── transition_gift.go ├── sceneLoop.go ├── sceneLoop_test.go ├── scene_test.go ├── screenFilter.go ├── screenFilter_test.go ├── screenshot.go ├── screenshot_test.go ├── shake ├── README.md └── shake.go ├── shape ├── bezier.go ├── bezier_test.go ├── condense.go ├── condense_test.go ├── doc.go ├── eq.go ├── holes.go ├── holes_test.go ├── in.go ├── outline.go ├── outline_test.go ├── points.go ├── points_test.go ├── rect.go ├── rect_test.go └── shape.go ├── shiny ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── PATENTS ├── README.md ├── doc.go ├── driver │ ├── androiddriver │ │ ├── image.go │ │ ├── main.go │ │ ├── screen.go │ │ └── texture.go │ ├── doc.go │ ├── driver.go │ ├── driver_android.go │ ├── driver_fallback.go │ ├── driver_js.go │ ├── driver_noop.go │ ├── driver_windows.go │ ├── driver_x11.go │ ├── internal │ │ ├── drawer │ │ │ └── drawer.go │ │ ├── errscreen │ │ │ └── errscreen.go │ │ ├── event │ │ │ └── event.go │ │ ├── lifecycler │ │ │ └── lifecycler.go │ │ ├── swizzle │ │ │ ├── swizzle_amd64.go │ │ │ ├── swizzle_amd64.s │ │ │ ├── swizzle_common.go │ │ │ ├── swizzle_other.go │ │ │ └── swizzle_test.go │ │ ├── win32 │ │ │ ├── cursor.go │ │ │ ├── key.go │ │ │ ├── syscall_windows.go │ │ │ ├── types.go │ │ │ ├── util.go │ │ │ ├── win32.go │ │ │ └── zsyscall_windows.go │ │ ├── x11 │ │ │ └── x11.go │ │ └── x11key │ │ │ ├── gen.go │ │ │ ├── table.go │ │ │ └── x11key.go │ ├── jsdriver │ │ ├── canvas.go │ │ ├── image.go │ │ ├── screen.go │ │ ├── texture.go │ │ └── window.go │ ├── mtldriver │ │ ├── bgra.go │ │ ├── buffer.go │ │ ├── internal │ │ │ ├── appkit │ │ │ │ ├── appkit.go │ │ │ │ ├── appkit.h │ │ │ │ └── appkit.m │ │ │ └── coreanim │ │ │ │ ├── coreanim.go │ │ │ │ ├── coreanim.h │ │ │ │ └── coreanim.m │ │ ├── mtldriver.go │ │ ├── screen.go │ │ ├── texture.go │ │ ├── window.go │ │ ├── window_amd64.go │ │ └── window_arm64.go │ ├── mtldriver_darwin.go │ ├── noop │ │ └── noop.go │ ├── windriver │ │ ├── buffer.go │ │ ├── doc.go │ │ ├── ico.go │ │ ├── other.go │ │ ├── screen.go │ │ ├── syscall.go │ │ ├── syscall_windows.go │ │ ├── texture.go │ │ ├── window.go │ │ ├── windraw.go │ │ ├── windriver.go │ │ └── zsyscall_windows.go │ └── x11driver │ │ ├── buffer.go │ │ ├── screen.go │ │ ├── shm_linux_ipc.go │ │ ├── shm_openbsd_syscall.go │ │ ├── shm_other.go │ │ ├── shm_shmopen_syscall.go │ │ ├── texture.go │ │ ├── window.go │ │ └── x11driver.go └── screen │ ├── drawer.go │ ├── event.go │ ├── image.go │ ├── options.go │ ├── screen.go │ ├── spanner.go │ ├── texture.go │ ├── utf.go │ └── utf_test.go ├── test_coverage.sh ├── test_examples.sh ├── test_examples_js.sh ├── testdata ├── audio │ └── placeholder.txt ├── default.config ├── images │ └── placeholder.txt └── screenshot.png ├── tidy_all.sh ├── timing ├── README.md ├── fps.go └── fps_test.go ├── viewport.go ├── viewport_test.go ├── window.go ├── window ├── README.md └── window.go └── window_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.apk 7 | 8 | # WASM test output files 9 | *.wasm 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | coverage.txt 18 | 19 | # Workspace configuration 20 | .vscode -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please raise issues for discussion before making significant pull requests to 4 | packages not under `oak/entities/x`. 5 | -------------------------------------------------------------------------------- /alg/degrees.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | import "math" 4 | 5 | const ( 6 | // DegToRad is the constant value something in 7 | // degrees should be multiplied by to obtain 8 | // something in radians. 9 | DegToRad = math.Pi / 180 10 | // RadToDeg is the constant value something in 11 | // radians should be multiplied by to obtain 12 | // something in degrees. 13 | RadToDeg = 180 / math.Pi 14 | ) 15 | 16 | // A Radian value is a float that specifies it should be in radians. 17 | type Radian float64 18 | 19 | // Degrees converts a Radian to Degrees. 20 | func (r Radian) Degrees() Degree { 21 | return Degree(r * RadToDeg) 22 | } 23 | 24 | // A Degree value is a float that specifies it should be in degrees. 25 | type Degree float64 26 | 27 | // Radians converts a Degree to Radians. 28 | func (d Degree) Radians() Radian { 29 | return Radian(d * DegToRad) 30 | } 31 | -------------------------------------------------------------------------------- /alg/degrees_test.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestDegrees(t *testing.T) { 9 | t.Parallel() 10 | t.Run("DegreeToRadians", func(t *testing.T) { 11 | t.Parallel() 12 | var f Degree = 90 13 | if Radian(f*DegToRad) != f.Radians() { 14 | t.Fatalf("degree to radian identity failed") 15 | } 16 | }) 17 | t.Run("RadianToDegrees", func(t *testing.T) { 18 | t.Parallel() 19 | var f2 Radian = math.Pi / 2 20 | if Degree(f2*RadToDeg) != f2.Degrees() { 21 | t.Fatalf("radian to degree identity failed") 22 | } 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /alg/doc.go: -------------------------------------------------------------------------------- 1 | // Package alg provides algorithms and math utilities. 2 | package alg 3 | -------------------------------------------------------------------------------- /alg/floatgeom/dir.go: -------------------------------------------------------------------------------- 1 | package floatgeom 2 | 3 | // Dir2 is a helper type for representing points as directions 4 | type Dir2 Point2 5 | 6 | // Dir2 values 7 | var ( 8 | Up = Dir2(Point2{0, -1}) 9 | Down = Dir2(Point2{0, 1}) 10 | Left = Dir2(Point2{-1, 0}) 11 | Right = Dir2(Point2{1, 0}) 12 | UpRight = Up.And(Right) 13 | DownRight = Down.And(Right) 14 | DownLeft = Down.And(Left) 15 | UpLeft = Up.And(Left) 16 | ) 17 | 18 | // And combines two directions 19 | func (d Dir2) And(d2 Dir2) Dir2 { 20 | return Dir2(Point2(d).Add(Point2(d2))) 21 | } 22 | 23 | // X retrieves the horizontal component of a Dir2 24 | func (d Dir2) X() float64 { 25 | return Point2(d).X() 26 | } 27 | 28 | // Y retrieves the vertical component for a Dir2 29 | func (d Dir2) Y() float64 { 30 | return Point2(d).Y() 31 | } 32 | -------------------------------------------------------------------------------- /alg/floatgeom/dir_test.go: -------------------------------------------------------------------------------- 1 | package floatgeom 2 | 3 | import "testing" 4 | 5 | func TestDirMethods(t *testing.T) { 6 | d := Dir2{10.0, 12.0} 7 | if d.X() != 10.0 { 8 | t.Fatalf("expected 10 for x, got %v", d.X()) 9 | } 10 | if d.Y() != 12.0 { 11 | t.Fatalf("expected 12 for y, got %v", d.Y()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /alg/floatgeom/doc.go: -------------------------------------------------------------------------------- 1 | // Package floatgeom provides primitives for floating point geometry. 2 | package floatgeom 3 | -------------------------------------------------------------------------------- /alg/floatgeom/triangle.go: -------------------------------------------------------------------------------- 1 | package floatgeom 2 | 3 | //Tri3 is a triangle of Point3s 4 | type Tri3 [3]Point3 5 | 6 | // Barycentric finds the barycentric coordinates of the given x,y cartesian 7 | // coordinates within this triangle. If the point (x,y) is outside of the 8 | // triangle, one of the output values will be negative. 9 | // Credit goes to github.com/yellingintothefan for their work in gel 10 | func (t Tri3) Barycentric(x, y float64) Point3 { 11 | p := Point3{x, y, 0.0} 12 | v0 := t[1].Sub(t[0]) 13 | v1 := t[2].Sub(t[0]) 14 | v2 := p.Sub(t[0]) 15 | d00 := v0.Dot(v0) 16 | d01 := v0.Dot(v1) 17 | d11 := v1.Dot(v1) 18 | d20 := v2.Dot(v0) 19 | d21 := v2.Dot(v1) 20 | v := (d11*d20 - d01*d21) / (d00*d11 - d01*d01) 21 | w := (d00*d21 - d01*d20) / (d00*d11 - d01*d01) 22 | u := 1.0 - v - w 23 | return Point3{v, w, u} 24 | } 25 | 26 | // Normal calculates the surface normal of a triangle 27 | func (t Tri3) Normal() Point3 { 28 | u := t[1].Sub(t[0]) 29 | v := t[2].Sub(t[0]) 30 | // Check that the triangle is defined in a clockwise fashion. 31 | 32 | return u.Cross(v).Normalize() 33 | } 34 | -------------------------------------------------------------------------------- /alg/floatgeom/triangle_test.go: -------------------------------------------------------------------------------- 1 | package floatgeom 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTriangleNormal(t *testing.T) { 8 | a := Tri3{ 9 | Point3{0, 0, 0}, 10 | Point3{1, 0, 0}, 11 | Point3{0, 1, 0}, 12 | } 13 | e := Point3{0, 0, 1} 14 | if e != a.Normal() { 15 | t.Fatalf("expected %v got %v", e, a.Normal()) 16 | } 17 | } 18 | 19 | func TestTriangleBarycentric(t *testing.T) { 20 | a := Tri3{ 21 | Point3{0, 0, 0}, 22 | Point3{1, 0, 0}, 23 | Point3{0, 1, 0}, 24 | } 25 | e := Point3{1, 1, -1} 26 | if e != a.Barycentric(1, 1) { 27 | t.Fatalf("expected %v got %v", e, a.Barycentric(1, 1)) 28 | } 29 | e = Point3{0.5, 0.5, 0} 30 | if e != a.Barycentric(.5, .5) { 31 | t.Fatalf("expected %v got %v", e, a.Barycentric(.5, .5)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /alg/intgeom/dir.go: -------------------------------------------------------------------------------- 1 | package intgeom 2 | 3 | // Dir2 is a helper type for representing points as directions 4 | type Dir2 Point2 5 | 6 | // Dir2 values 7 | var ( 8 | Up = Dir2(Point2{0, -1}) 9 | Down = Dir2(Point2{0, 1}) 10 | Left = Dir2(Point2{-1, 0}) 11 | Right = Dir2(Point2{1, 0}) 12 | UpRight = Up.And(Right) 13 | DownRight = Down.And(Right) 14 | DownLeft = Down.And(Left) 15 | UpLeft = Up.And(Left) 16 | ) 17 | 18 | // And combines two directions 19 | func (d Dir2) And(d2 Dir2) Dir2 { 20 | return Dir2(Point2(d).Add(Point2(d2))) 21 | } 22 | 23 | // X retrieves the horizontal component of a Dir2 24 | func (d Dir2) X() int { 25 | return Point2(d).X() 26 | } 27 | 28 | // Y retrieves the vertical component for a Dir2 29 | func (d Dir2) Y() int { 30 | return Point2(d).Y() 31 | } 32 | -------------------------------------------------------------------------------- /alg/intgeom/dir_test.go: -------------------------------------------------------------------------------- 1 | package intgeom 2 | 3 | import "testing" 4 | 5 | func TestDirMethods(t *testing.T) { 6 | d := Dir2{10, 12} 7 | if d.X() != 10 { 8 | t.Fatalf("expected 10 for x, got %v", d.X()) 9 | } 10 | if d.Y() != 12 { 11 | t.Fatalf("expected 12 for y, got %v", d.Y()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /alg/intgeom/doc.go: -------------------------------------------------------------------------------- 1 | // Package intgeom provides primitives for integer geometry. 2 | package intgeom 3 | -------------------------------------------------------------------------------- /alg/math.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // RoundF64 rounds a float64 to an int 8 | func RoundF64(a float64) int { 9 | if a < 0 { 10 | return int(math.Ceil(a - 0.5)) 11 | } 12 | return int(math.Floor(a + 0.5)) 13 | } 14 | 15 | const ( 16 | ε = 1.0e-7 17 | ) 18 | 19 | // F64eq equates two float64s within a small epsilon. 20 | func F64eq(f1, f2 float64) bool { 21 | return F64eqEps(f1, f2, ε) 22 | } 23 | 24 | // F64eqEps equates two float64s within a provided epsilon. 25 | func F64eqEps(f1, f2, epsilon float64) bool { 26 | return math.Abs(f1-f2) <= epsilon 27 | } 28 | -------------------------------------------------------------------------------- /alg/math_test.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | import "testing" 4 | 5 | func TestRoundF64(t *testing.T) { 6 | 7 | inputs := []float64{ 8 | 0.1, 9 | 0.7, 10 | -1.2, 11 | -1.7, 12 | } 13 | outputs := []int{ 14 | 0, 15 | 1, 16 | -1, 17 | -2, 18 | } 19 | 20 | for i, in := range inputs { 21 | if RoundF64(in) != outputs[i] { 22 | t.Fail() 23 | } 24 | } 25 | } 26 | 27 | func TestF64Eq(t *testing.T) { 28 | inputs := [][]float64{ 29 | {0.000000001, 0}, 30 | } 31 | outputs := []bool{ 32 | true, 33 | } 34 | for i, in := range inputs { 35 | if F64eq(in[0], in[1]) != outputs[i] { 36 | t.Fail() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /alg/span/color.go: -------------------------------------------------------------------------------- 1 | package span 2 | 3 | import "image/color" 4 | 5 | type linearColor struct { 6 | r, g, b, a Span[uint32] 7 | } 8 | 9 | // NewLinearColor returns a linear color distribution between min and maxColor 10 | func NewLinearColor(minColor, maxColor color.Color) Span[color.Color] { 11 | r, g, b, a := minColor.RGBA() 12 | r2, g2, b2, a2 := maxColor.RGBA() 13 | return linearColor{ 14 | NewLinear(r, r2), 15 | NewLinear(g, g2), 16 | NewLinear(b, b2), 17 | NewLinear(a, a2), 18 | } 19 | } 20 | 21 | func (l linearColor) Clamp(c color.Color) color.Color { 22 | r3, g3, b3, a3 := c.RGBA() 23 | r4 := l.r.Clamp(r3) 24 | g4 := l.g.Clamp(g3) 25 | b4 := l.b.Clamp(b3) 26 | a4 := l.a.Clamp(a3) 27 | return rgbaFromInts(r4, g4, b4, a4) 28 | } 29 | 30 | func (l linearColor) MulSpan(i float64) Span[color.Color] { 31 | return linearColor{ 32 | l.r.MulSpan(i), 33 | l.g.MulSpan(i), 34 | l.b.MulSpan(i), 35 | l.a.MulSpan(i), 36 | } 37 | } 38 | 39 | func (l linearColor) Poll() color.Color { 40 | r3 := l.r.Poll() 41 | g3 := l.g.Poll() 42 | b3 := l.b.Poll() 43 | a3 := l.a.Poll() 44 | return rgbaFromInts(r3, g3, b3, a3) 45 | } 46 | 47 | func (l linearColor) Percentile(f float64) color.Color { 48 | r3 := l.r.Percentile(f) 49 | g3 := l.g.Percentile(f) 50 | b3 := l.b.Percentile(f) 51 | a3 := l.a.Percentile(f) 52 | return rgbaFromInts(r3, g3, b3, a3) 53 | } 54 | 55 | func rgbaFromInts(r, g, b, a uint32) color.RGBA { 56 | return color.RGBA{uint8(r / 257), uint8(g / 257), uint8(b / 257), uint8(a / 257)} 57 | } 58 | -------------------------------------------------------------------------------- /alg/span/doc.go: -------------------------------------------------------------------------------- 1 | // Package span provides helper constructs to represent ranges of values, to poll from or clamp to 2 | package span 3 | -------------------------------------------------------------------------------- /alg/span/internal/random/rand.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | var seed int64 10 | 11 | func init() { 12 | seed = time.Now().UTC().UnixNano() 13 | } 14 | 15 | func Rand() *rand.Rand { 16 | return rand.New(rand.NewSource(atomic.AddInt64(&seed, 1))) 17 | } 18 | -------------------------------------------------------------------------------- /alg/span/span.go: -------------------------------------------------------------------------------- 1 | package span 2 | 3 | // A Span represents some enumerable range. 4 | type Span[T any] interface { 5 | // Poll returns a pseudorandom value within this span. 6 | Poll() T 7 | // Clamp, if v lies within the boundary of this span, returns v. 8 | // Otherwise, CLamp returns a modified version of v that is rounded to the closest value 9 | // that does lie within the boundary of this span. 10 | Clamp(v T) T 11 | // Percentile returns the value along this span that is at the provided percentile through the span, 12 | // e.g. providing .5 will return the middle of the span, providing 1 will return the maximum value in 13 | // the span. Providing a value less than 0 or greater than 1 may extend the span by where it would theoretically 14 | // progress, but should not be relied upon unless a given implementation specifies what it will do. If this span 15 | // represents multiple degrees of freedom, this will pin all those degrees to the single provided percent. 16 | Percentile(float64) T 17 | // MulSpan returns this span with its entire range multiplied by the given constant. 18 | MulSpan(float64) Span[T] 19 | } 20 | -------------------------------------------------------------------------------- /alg/stwHeap.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | type stwHeap struct { 4 | bh []float64 5 | weightsBelow []float64 6 | } 7 | 8 | // Select Total Weight Heap 9 | // This name was chosen relatively arbitrarily, if there 10 | // is a canonical academic name for this structure we'd gladly 11 | // use that instead 12 | func newSTWHeap(f []float64) *stwHeap { 13 | stwh := new(stwHeap) 14 | f = append([]float64{0}, f...) 15 | // The order of elements literally does not 16 | // matter, so 'heap' is a misnomer. 17 | stwh.bh = f 18 | stwh.weightsBelow = make([]float64, len(f)) 19 | copy(stwh.weightsBelow, f) 20 | for i := len(f) - 1; i > 1; i-- { 21 | stwh.weightsBelow[i>>1] += stwh.weightsBelow[i] 22 | } 23 | return stwh 24 | } 25 | 26 | func (stwh *stwHeap) Pop(rng float64) int { 27 | if stwh.weightsBelow[1] <= ε { 28 | return -1 29 | } 30 | w := stwh.weightsBelow[1] * rng 31 | i := 1 32 | 33 | // With the >= here, we don't accept 0 weights 34 | for w >= stwh.bh[i] { 35 | w -= stwh.bh[i] 36 | i <<= 1 // Propagate to left child 37 | if w >= stwh.weightsBelow[i] { 38 | w -= stwh.weightsBelow[i] 39 | i++ // Switch to right child 40 | } 41 | } 42 | i2 := i 43 | w = stwh.bh[i] 44 | // Instead of removing a node we set its weight to 0. 45 | stwh.bh[i] = 0 46 | 47 | // All parents of the index need to be reduced 48 | // in total weight. 49 | for i > 0 { 50 | stwh.weightsBelow[i] -= w 51 | i >>= 1 52 | } 53 | return i2 - 1 54 | } 55 | -------------------------------------------------------------------------------- /alg/triangulate.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | // TriangulateConvex takes a face, in the form of a slice 4 | // of indices, and outputs those indicies split into triangles 5 | // based on the assumption that the face is convex. This involves 6 | // forming pairs of indices and drawing an edge back to the first 7 | // index repeatedly. 8 | // 9 | // If given less than 3 indices, returns an empty slice. 10 | // 11 | // Example input: [0,1,2,3,4] 12 | // Example output: [[0,1,2][0,2,3][0,3,4]] 13 | // 14 | // Visual Example: 15 | // ____0____ 16 | // 4 1 17 | // \ / 18 | // 3-----2 19 | // - - - 20 | // ____0____ 21 | // 4 / \ 1 22 | // \ / \ / 23 | // 3-----2 24 | // 25 | // This makes additional assumptions that the points represented 26 | // by the indices are coplanar, and that there are no holes present 27 | // in the face. 28 | func TriangulateConvex(face []int) [][3]int { 29 | if len(face) < 3 { 30 | return [][3]int{} 31 | } 32 | tris := make([][3]int, len(face)-2) 33 | for i := 0; i < len(tris); i++ { 34 | tris[i][0] = face[0] 35 | tris[i][1] = face[i+1] 36 | tris[i][2] = face[i+2] 37 | } 38 | return tris 39 | } 40 | -------------------------------------------------------------------------------- /alg/triangulate_test.go: -------------------------------------------------------------------------------- 1 | package alg 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestTriangulateConvex(t *testing.T) { 10 | t.Parallel() 11 | type testCase struct { 12 | in []int 13 | out [][3]int 14 | } 15 | testCases := []testCase{ 16 | { 17 | []int{0, 1}, 18 | [][3]int{}, 19 | }, 20 | { 21 | []int{0}, 22 | [][3]int{}, 23 | }, 24 | { 25 | []int{0, 1, 2}, 26 | [][3]int{{0, 1, 2}}, 27 | }, 28 | { 29 | []int{0, 1, 2, 3, 4}, 30 | [][3]int{{0, 1, 2}, {0, 2, 3}, {0, 3, 4}}, 31 | }, 32 | } 33 | for i, tc := range testCases { 34 | tc := tc 35 | t.Run(strconv.Itoa(i), func(t *testing.T) { 36 | t.Parallel() 37 | out := TriangulateConvex(tc.in) 38 | if !reflect.DeepEqual(tc.out, out) { 39 | t.Fatalf("expected %v got %v", tc.out, out) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /audio/driver.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // A Driver defines the underlying interface that should be used for initializing PCM audio writers. 4 | type Driver int 5 | 6 | const ( 7 | // DriverDefault indicates to this package to use a default driver based on the OS. 8 | // Currently, for windows the default is DirectSound and for unix the default is PulseAudio. 9 | DriverDefault Driver = iota 10 | DriverPulse 11 | DriverDirectSound 12 | DriverALSA 13 | ) 14 | 15 | var driverNames = map[Driver]string{ 16 | DriverPulse: "pulseaudio", 17 | DriverDirectSound: "directsound", 18 | DriverDefault: "default", 19 | DriverALSA: "alsa", 20 | } 21 | 22 | func (d Driver) String() string { 23 | return driverNames[d] 24 | } 25 | -------------------------------------------------------------------------------- /audio/driver_test.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "testing" 4 | 5 | func TestDriver_String(t *testing.T) { 6 | drivers := []Driver{ 7 | DriverDefault, 8 | DriverDirectSound, 9 | DriverPulse, 10 | DriverALSA, 11 | } 12 | for _, d := range drivers { 13 | if d.String() == "" { 14 | t.Errorf("driver %d had no defined string", d) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /audio/file_cache.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import ( 4 | "path/filepath" 5 | "sync" 6 | 7 | "github.com/oakmound/oak/v4/audio/pcm" 8 | ) 9 | 10 | // DefaultCache is the receiver for package level loading operations. 11 | var DefaultCache = NewCache() 12 | 13 | // Cache is a simple audio data cache 14 | type Cache struct { 15 | mu sync.RWMutex 16 | data map[string]*BytesReader 17 | } 18 | 19 | // NewCache returns an empty Cache 20 | func NewCache() *Cache { 21 | return &Cache{ 22 | data: make(map[string]*BytesReader), 23 | } 24 | } 25 | 26 | // ClearAll will remove all elements from a Cache 27 | func (c *Cache) ClearAll() { 28 | c.mu.Lock() 29 | c.data = make(map[string]*BytesReader) 30 | c.mu.Unlock() 31 | } 32 | 33 | // Clear will remove elements matching the given key from the Cache. 34 | func (c *Cache) Clear(key string) { 35 | c.mu.Lock() 36 | delete(c.data, key) 37 | c.mu.Unlock() 38 | } 39 | 40 | func (c *Cache) setLoaded(file string, r pcm.Reader) { 41 | // This ReadAll and .Copy() on Cache.Read ensure that multiple loads from the cache do not 42 | // change the data that will be read on future reads. 43 | br := ReadAll(r) 44 | c.mu.Lock() 45 | c.data[file] = br 46 | c.data[filepath.Base(file)] = br 47 | c.mu.Unlock() 48 | } 49 | 50 | // Load calls Load on the Default Cache. 51 | func Load(file string) (pcm.Reader, error) { 52 | return DefaultCache.Load(file) 53 | } 54 | 55 | // Get calls Get on the Default Cache. 56 | func Get(file string) (pcm.Reader, error) { 57 | return DefaultCache.Get(file) 58 | } 59 | -------------------------------------------------------------------------------- /audio/format/ceol/testdata/test.ceol: -------------------------------------------------------------------------------- 1 | 3,0,0,0,150,32,4,2,134,0,1,128,0,256,141,0,20,128,0,256,3,10,2,0,1,26,77,1,0,0,73,1,1,0,70,1,2,0,63,1,4,0,60,1,5,0,84,1,0,0,81,1,1,0,77,1,2,0,73,1,3,0,70,1,4,0,67,1,5,0,63,1,6,0,60,1,7,0,57,1,6,0,53,1,7,0,67,1,3,0,82,1,0,0,77,1,1,0,72,1,2,0,70,1,3,0,69,1,4,0,63,1,5,0,58,1,6,0,57,1,7,0,34,32,16,0,41,32,16,0,0,0,0,1,20,10,70,1,0,0,69,1,0,0,66,1,2,0,64,1,2,0,59,1,4,0,58,1,4,0,57,1,4,0,54,1,6,0,49,1,9,0,48,1,9,0,0,0,0,0,1,0,0,2,0,2,0,-1,-1,-1,-1,-1,-1,-1,2,-1,-1,-1,-1,-1,-1,-1, -------------------------------------------------------------------------------- /audio/format/dls/testdata/SanbikiSCC.dls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/audio/format/dls/testdata/SanbikiSCC.dls -------------------------------------------------------------------------------- /audio/format/mp3/mp3.go: -------------------------------------------------------------------------------- 1 | // Package mp3 provides functionality to handle .mp3 files and .mp3 encoded data. 2 | // 3 | // This package may be imported solely to register mp3s as a parseable file type within oak: 4 | // 5 | // import ( 6 | // _ "github.com/oakmound/oak/v4/audio/format/mp3" 7 | // ) 8 | // 9 | package mp3 10 | 11 | import ( 12 | "io" 13 | 14 | "github.com/oakmound/oak/v4/audio/format" 15 | "github.com/oakmound/oak/v4/audio/pcm" 16 | 17 | "github.com/hajimehoshi/go-mp3" 18 | ) 19 | 20 | func init() { 21 | format.Register(".mp3", Load) 22 | } 23 | 24 | // Load reads MP3 data from a reader, parsing it's PCM format and returning 25 | // a pcm Reader for the data contained within. It will error if the reader 26 | // does not contain enough data to fill a file header. The resulting format 27 | // will always be 16 bits and 2 channels. 28 | func Load(r io.Reader) (pcm.Reader, error) { 29 | d, err := mp3.NewDecoder(r) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &pcm.IOReader{ 34 | Format: pcm.Format{ 35 | SampleRate: uint32(d.SampleRate()), 36 | Bits: 16, 37 | Channels: 2, 38 | }, 39 | Reader: d, 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /audio/format/register.go: -------------------------------------------------------------------------------- 1 | // Package format provides audio file and format parsers 2 | package format 3 | 4 | import ( 5 | "io" 6 | "sync" 7 | 8 | "github.com/oakmound/oak/v4/audio/pcm" 9 | ) 10 | 11 | // A Loader can parse the data from an io.Reader and convert it into PCM encoded audio data with 12 | // a known format. 13 | type Loader func(r io.Reader) (pcm.Reader, error) 14 | 15 | var fileLoadersLock sync.RWMutex 16 | var fileLoaders = map[string]func(r io.Reader) (pcm.Reader, error){} 17 | 18 | // Register registers a format by file extension (eg '.mp3') with its parsing function. 19 | func Register(extension string, fn Loader) { 20 | fileLoadersLock.Lock() 21 | fileLoaders[extension] = fn 22 | fileLoadersLock.Unlock() 23 | } 24 | 25 | // LoaderForExtension returns a previously registered loader. 26 | func LoaderForExtension(extension string) (Loader, bool) { 27 | fileLoadersLock.RLock() 28 | defer fileLoadersLock.RUnlock() 29 | loader, ok := fileLoaders[extension] 30 | return loader, ok 31 | } 32 | -------------------------------------------------------------------------------- /audio/format/riff/info.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | // INFO is a common RIFF component. Most of these fields will be absent on 4 | // any given INFO struct. Todo: consider if these should be given names 5 | // that are informative instead of representative of their structural tag 6 | type INFO struct { 7 | // Arhcival Location 8 | IARL string `riff:"IARL"` 9 | // Arist 10 | IART string `riff:"IART"` 11 | // Commissioned By 12 | ICMS string `riff:"ICMS"` 13 | // Comments 14 | ICMT string `riff:"ICMT"` 15 | // Copyright 16 | ICOP string `riff:"ICOP"` 17 | // Creation Date 18 | ICRD string `riff:"ICRD"` 19 | // Engineer 20 | IENG string `riff:"IENG"` 21 | // Genre 22 | IGNR string `riff:"IGNR"` 23 | // Keywords 24 | IKEY string `riff:"IKEY"` 25 | // Medium 26 | IMED string `riff:"IMED"` 27 | // Name 28 | INAM string `riff:"INAM"` 29 | // Product 30 | IPRD string `riff:"IPRD"` 31 | // Subject 32 | ISBJ string `riff:"ISBJ"` 33 | // Software 34 | ISFT string `riff:"ISFT"` 35 | // Source 36 | ISRC string `riff:"ISRC"` 37 | // Source Form 38 | ISRF string `riff:"ISRF"` 39 | // Technician 40 | ITCH string `riff:"ITCH"` 41 | } 42 | -------------------------------------------------------------------------------- /audio/format/wav/testdata/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/audio/format/wav/testdata/test.wav -------------------------------------------------------------------------------- /audio/init_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package audio 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/jfreymuth/pulse" 9 | "github.com/oakmound/oak/v4/audio/pcm" 10 | "github.com/oakmound/oak/v4/oakerr" 11 | ) 12 | 13 | func initOS(driver Driver) error { 14 | switch driver { 15 | case DriverDefault: 16 | fallthrough 17 | case DriverPulse: 18 | // Sanity check that pulse is installed and a sink is defined 19 | client, err := pulse.NewClient() 20 | if err != nil { 21 | // osx: brew install pulseaudio 22 | // linux: sudo apt install pulseaudio 23 | return oakerr.UnsupportedPlatform{ 24 | Operation: "pcm.Init:" + driver.String(), 25 | } 26 | } 27 | defer client.Close() 28 | _, err = client.DefaultSink() 29 | if err != nil { 30 | return err 31 | } 32 | newWriter = newPulseWriter 33 | default: 34 | return oakerr.UnsupportedPlatform{ 35 | Operation: "pcm.Init:" + driver.String(), 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | var newWriter = func(f pcm.Format) (pcm.Writer, error) { 42 | return nil, fmt.Errorf("this package has not been initialized") 43 | } 44 | -------------------------------------------------------------------------------- /audio/init_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package audio 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/jfreymuth/pulse" 10 | "github.com/oakmound/oak/v4/audio/pcm" 11 | "github.com/oakmound/oak/v4/oakerr" 12 | ) 13 | 14 | func initOS(driver Driver) error { 15 | switch driver { 16 | case DriverDefault: 17 | fallthrough 18 | case DriverPulse: 19 | // Sanity check that pulse is installed and a sink is defined 20 | client, err := pulse.NewClient() 21 | if err != nil { 22 | // osx: brew install pulseaudio 23 | // linux: sudo apt install pulseaudio 24 | return oakerr.UnsupportedPlatform{ 25 | Operation: "pcm.Init:" + driver.String(), 26 | } 27 | } 28 | defer client.Close() 29 | _, err = client.DefaultSink() 30 | if err != nil { 31 | return err 32 | } 33 | newWriter = newPulseWriter 34 | case DriverALSA: 35 | //??? 36 | newWriter = newALSAWriter 37 | if skipDevices := os.Getenv("OAK_SKIP_AUDIO_DEVICES"); skipDevices != "" { 38 | SkipDevicesContaining = skipDevices 39 | } 40 | default: 41 | return oakerr.UnsupportedPlatform{ 42 | Operation: "pcm.Init:" + driver.String(), 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | var newWriter = func(f pcm.Format) (pcm.Writer, error) { 49 | return nil, fmt.Errorf("this package has not been initialized") 50 | } 51 | 52 | // TODO: do other drivers need this? Can we pick devices more intelligently? 53 | var SkipDevicesContaining string = "HDMI" 54 | -------------------------------------------------------------------------------- /audio/synth/filter_test.go: -------------------------------------------------------------------------------- 1 | package synth 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/oakmound/oak/v4/audio" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | err := audio.InitDefault() 14 | if err != nil { 15 | panic(err) 16 | } 17 | os.Exit(m.Run()) 18 | } 19 | 20 | func TestFilters(t *testing.T) { 21 | src := Int16 22 | // Todo: really gotta fix the sample rate evenness thing 23 | src.SampleRate = 40000 24 | src.Volume = .07 25 | 26 | fadeInFrames := time.Second 27 | 28 | unison := 4 29 | 30 | for i := 0; i < unison; i++ { 31 | go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Saw()))) 32 | go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Saw(Detune(.04))))) 33 | go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Saw(Detune(-.05))))) 34 | } 35 | go audio.Play(context.Background(), audio.FadeIn(fadeInFrames, audio.LoopReader(src.Noise()))) 36 | 37 | time.Sleep(3 * time.Second) 38 | } 39 | -------------------------------------------------------------------------------- /audio/synth/source.go: -------------------------------------------------------------------------------- 1 | package synth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/oakmound/oak/v4/audio/pcm" 7 | ) 8 | 9 | // A Source stores necessary information for generating waveform data 10 | type Source struct { 11 | pcm.Format 12 | Pitch Pitch 13 | // Volume, between 0.0 -> 1.0 14 | Volume float64 15 | Seconds float64 16 | } 17 | 18 | // PlayLength returns the time it will take before audio generated from this 19 | // source will stop. 20 | func (s Source) PlayLength() time.Duration { 21 | return time.Duration(s.Seconds) * 1000 * time.Millisecond 22 | } 23 | 24 | // Phase is shorthand for phase(s.Pitch, i, s.SampleRate). 25 | // Some sources might have custom phase functions in the future, however. 26 | func (s Source) Phase(i int) float64 { 27 | return phase(s.Pitch, i, s.SampleRate) 28 | } 29 | 30 | // Update is shorthand for applying a set of options to a source 31 | func (s Source) Update(opts ...Option) Source { 32 | for _, opt := range opts { 33 | s = opt(s) 34 | } 35 | return s 36 | } 37 | 38 | var ( 39 | // Int16 is a default source for building 16-bit audio 40 | Int16 = Source{ 41 | Format: pcm.Format{ 42 | SampleRate: 44100, 43 | Channels: 2, 44 | // within a source, if Bits is not specified, it'll default to 16. 45 | Bits: 16, 46 | }, 47 | Pitch: A4, 48 | Volume: .25, 49 | Seconds: 1, 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /audio/testdata/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/audio/testdata/test.wav -------------------------------------------------------------------------------- /audio/writer.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4/audio/pcm" 5 | ) 6 | 7 | // NewWriter returns a writer which can accept audio streamed matching the given format 8 | func NewWriter(f pcm.Format) (pcm.Writer, error) { 9 | return newWriter(f) 10 | } 11 | 12 | // MustNewWriter calls NewWriter and panics if an error is returned. 13 | func MustNewWriter(f pcm.Format) pcm.Writer { 14 | w, err := NewWriter(f) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return w 19 | } 20 | -------------------------------------------------------------------------------- /audio/writer_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !linux && !darwin 2 | 3 | package audio 4 | 5 | import ( 6 | "github.com/oakmound/oak/v4/audio/pcm" 7 | "github.com/oakmound/oak/v4/oakerr" 8 | ) 9 | 10 | func initOS(driver Driver) error { 11 | return oakerr.UnsupportedPlatform{ 12 | Operation: "pcm.Init", 13 | } 14 | } 15 | 16 | func newWriter(f pcm.Format) (pcm.Writer, error) { 17 | return nil, oakerr.UnsupportedPlatform{ 18 | Operation: "pcm.NewWriter", 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /collision/doc.go: -------------------------------------------------------------------------------- 1 | // Package collision provides collision tree and space structures along with 2 | // hit detection functions on spaces. 3 | package collision 4 | -------------------------------------------------------------------------------- /collision/label.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | const ( 4 | // NilLabel is used internally for spaces that are otherwise not 5 | // given labels. 6 | NilLabel Label = -1 7 | ) 8 | 9 | // Label is used to store type information for a given space 10 | type Label int 11 | -------------------------------------------------------------------------------- /collision/onCollision_test.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/oakmound/oak/v4/event" 8 | ) 9 | 10 | type cphase struct { 11 | Phase 12 | callers *event.CallerMap 13 | } 14 | 15 | func TestCollisionPhase(t *testing.T) { 16 | b := event.NewBus(event.NewCallerMap()) 17 | go func() { 18 | for { 19 | <-time.After(5 * time.Millisecond) 20 | <-event.TriggerOn(b, event.Enter, event.EnterPayload{}) 21 | } 22 | }() 23 | cp := &cphase{} 24 | cid := b.GetCallerMap().Register(cp) 25 | s := NewSpace(10, 10, 10, 10, cid) 26 | tree := NewTree() 27 | err := PhaseCollisionWithBus(s, tree, b) 28 | if err != nil { 29 | t.Fatalf("phase collision failed: %v", err) 30 | } 31 | activeCh := make(chan bool, 5) 32 | b1 := event.Bind(b, Start, cp, func(_ *cphase, _ Label) event.Response { 33 | activeCh <- true 34 | return 0 35 | }) 36 | b2 := event.Bind(b, Stop, cp, func(_ *cphase, _ Label) event.Response { 37 | activeCh <- false 38 | return 0 39 | }) 40 | <-b1.Bound 41 | <-b2.Bound 42 | s2 := NewLabeledSpace(15, 15, 10, 10, 5) 43 | tree.Add(s2) 44 | if active := <-activeCh; !active { 45 | t.Fatalf("collision should be active") 46 | } 47 | 48 | tree.Remove(s2) 49 | time.Sleep(200 * time.Millisecond) 50 | if active := <-activeCh; active { 51 | t.Fatalf("collision should be inactive") 52 | } 53 | } 54 | 55 | func TestPhaseCollision_Unembedded(t *testing.T) { 56 | t.Parallel() 57 | s3 := NewSpace(10, 10, 10, 10, 5) 58 | err := PhaseCollision(s3, nil) 59 | if err == nil { 60 | t.Fatalf("phase collision should have failed") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /collision/onHit.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | // An OnHit is a function which takes in two spaces 4 | type OnHit func(s, s2 *Space) 5 | 6 | // CallOnHits will send a signal to the passed in channel 7 | // when it has completed all collision functions in the hitmap. 8 | func CallOnHits(s *Space, onHits map[Label]OnHit, doneCh chan bool) { 9 | DefaultTree.CallOnHits(s, onHits, doneCh) 10 | } 11 | 12 | // CallOnHits will send a signal to the passed in channel 13 | // when it has completed all collision functions in the hitmap. 14 | func (t *Tree) CallOnHits(s *Space, onHits map[Label]OnHit, doneCh chan bool) { 15 | progCh := make(chan bool) 16 | hits := t.Hits(s) 17 | for _, s2 := range hits { 18 | go func(s, s2 *Space, onHits map[Label]OnHit, progCh chan bool) { 19 | if fn, ok := onHits[s2.Label]; ok { 20 | fn(s, s2) 21 | progCh <- true 22 | return 23 | } 24 | progCh <- false 25 | }(s, s2, onHits, progCh) 26 | } 27 | // This waits to send our signal that we've 28 | // finished until we've counted signals for 29 | // each collision entity 30 | hitFlag := false 31 | for range hits { 32 | v := <-progCh 33 | hitFlag = hitFlag || v 34 | } 35 | doneCh <- hitFlag 36 | } 37 | 38 | // OnIDs converts a function on two CIDs to an OnHit 39 | func OnIDs(fn func(int, int)) func(s, s2 *Space) { 40 | return func(s, s2 *Space) { 41 | fn(int(s.CID), int(s2.CID)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /collision/point.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | import "github.com/oakmound/oak/v4/alg/floatgeom" 4 | 5 | // A Point is a specific point where 6 | // collision occurred and a zone to identify 7 | // what was collided with. 8 | type Point struct { 9 | floatgeom.Point3 10 | Zone *Space 11 | } 12 | 13 | // NewPoint creates a new point 14 | func NewPoint(s *Space, x, y float64) Point { 15 | return Point{floatgeom.Point3{x, y, 0}, s} 16 | } 17 | 18 | // IsNil returns whether the underlying zone of a Point is nil 19 | func (cp Point) IsNil() bool { 20 | return cp.Zone == nil 21 | } 22 | -------------------------------------------------------------------------------- /collision/point_test.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewPoint(t *testing.T) { 8 | p := NewPoint(nil, 10, 10) 9 | if p.X() != 10 { 10 | t.Fatalf("bad x, expected %v got %v", 10, p.X()) 11 | } 12 | if p.Y() != 10 { 13 | t.Fatalf("bad y, expected %v got %v", 10, p.Y()) 14 | } 15 | if !p.IsNil() { 16 | t.Fatalf("nil point should have been nil") 17 | } 18 | p2 := NewPoint(&Space{}, 0, 0) 19 | if p2.IsNil() { 20 | t.Fatalf("set point should not have been nil") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /collision/ray/doc.go: -------------------------------------------------------------------------------- 1 | // Package ray holds utilities for performing iterative collision checks 2 | // or raycasts 3 | package ray 4 | -------------------------------------------------------------------------------- /collision/ray/raycast_test.go: -------------------------------------------------------------------------------- 1 | package ray 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/alg/floatgeom" 7 | "github.com/oakmound/oak/v4/alg/span" 8 | "github.com/oakmound/oak/v4/collision" 9 | ) 10 | 11 | func TestEmptyRaycasts(t *testing.T) { 12 | t.Skip() 13 | collision.DefaultTree.Clear() 14 | vRange := span.NewLinear(3.0, 359.0) 15 | tests := 100 16 | for i := 0; i < tests; i++ { 17 | p1 := floatgeom.Point2{vRange.Poll(), vRange.Poll()} 18 | p2 := floatgeom.Point2{vRange.Poll(), vRange.Poll()} 19 | if len(Cast(p1, p2)) != 0 { 20 | t.Fatalf("cast found a point in the empty tree") 21 | } 22 | if len(CastTo(p1, p2)) != 0 { 23 | t.Fatalf("cast to found a point in the empty tree") 24 | } 25 | if len(ConeCast(p1, p2)) != 0 { 26 | t.Fatalf("cone cast found a point in the empty tree") 27 | } 28 | if len(ConeCastTo(p1, p2)) != 0 { 29 | t.Fatalf("cone cast to found a point in the empty tree") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /collision/reactiveSpace.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | import "sync" 4 | 5 | // ReactiveSpace is a space that keeps track of a map of collision events 6 | type ReactiveSpace struct { 7 | *Space 8 | Tree *Tree 9 | 10 | onHitsLock sync.Mutex 11 | onHits map[Label]OnHit 12 | } 13 | 14 | // NewReactiveSpace creates a reactive space on the default collision tree 15 | func NewReactiveSpace(s *Space, onHits map[Label]OnHit) *ReactiveSpace { 16 | return &ReactiveSpace{ 17 | Space: s, 18 | Tree: DefaultTree, 19 | onHits: onHits, 20 | } 21 | } 22 | 23 | // CallOnHits calls CallOnHits on the underlying space of a reactive space 24 | // with the reactive spaces' map of collision events, and returns the channel 25 | // it will send the done signal from. It is not safe to call concurrently with 26 | // add / remove / clear. 27 | func (rs *ReactiveSpace) CallOnHits() chan bool { 28 | doneCh := make(chan bool) 29 | go rs.Tree.CallOnHits(rs.Space, rs.onHits, doneCh) 30 | return doneCh 31 | } 32 | 33 | // Add adds a mapping to a reactive spaces' onhit map 34 | func (rs *ReactiveSpace) Add(i Label, oh OnHit) { 35 | rs.onHitsLock.Lock() 36 | defer rs.onHitsLock.Unlock() 37 | rs.onHits[i] = oh 38 | } 39 | 40 | // Remove removes a mapping from a reactive spaces' onhit map 41 | func (rs *ReactiveSpace) Remove(i Label) { 42 | rs.onHitsLock.Lock() 43 | defer rs.onHitsLock.Unlock() 44 | delete(rs.onHits, i) 45 | } 46 | 47 | // Clear resets a reactive space's onhit map 48 | func (rs *ReactiveSpace) Clear() { 49 | rs.onHitsLock.Lock() 50 | defer rs.onHitsLock.Unlock() 51 | rs.onHits = make(map[Label]OnHit) 52 | } 53 | -------------------------------------------------------------------------------- /collision/reactiveSpace_test.go: -------------------------------------------------------------------------------- 1 | package collision 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReactiveSpace(t *testing.T) { 8 | Clear() 9 | var triggered bool 10 | rs1 := NewReactiveSpace(NewUnassignedSpace(0, 0, 10, 10), map[Label]OnHit{}) 11 | if rs1 == nil { 12 | t.Fatalf("reactive space was nil after creation") 13 | } 14 | rs2 := NewReactiveSpace(NewUnassignedSpace(5, 5, 10, 10), map[Label]OnHit{ 15 | Label(1): OnIDs(func(id1, id2 int) { 16 | triggered = true 17 | }), 18 | }) 19 | Add(NewLabeledSpace(6, 6, 1, 1, Label(1))) 20 | <-rs2.CallOnHits() 21 | if !triggered { 22 | t.Fatalf("CallOnHits did not trigger reactive space's callback") 23 | } 24 | triggered = false 25 | 26 | rs2.Clear() 27 | <-rs2.CallOnHits() 28 | if triggered { 29 | t.Fatalf("CallOnHits triggered reactive space's callback after it was cleared") 30 | } 31 | 32 | rs1.Add(Label(1), func(*Space, *Space) { 33 | triggered = true 34 | }) 35 | <-rs1.CallOnHits() 36 | if !triggered { 37 | t.Fatalf("CallOnHits did not trigger reactive space's callback (2)") 38 | } 39 | 40 | rs1.Remove(Label(1)) 41 | triggered = false 42 | <-rs1.CallOnHits() 43 | if triggered { 44 | t.Fatalf("CallOnHits triggered reactive space's callback after it was removed") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /collision/rtree-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Daniel Connelly. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | Neither the name of Daniel Connelly nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /debugstream/defaultcommands.go: -------------------------------------------------------------------------------- 1 | package debugstream 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync" 7 | 8 | "github.com/oakmound/oak/v4/window" 9 | ) 10 | 11 | var ( 12 | // DefaultCommands to attach to. 13 | DefaultCommands *ScopedCommands 14 | defaultsOnce sync.Once 15 | ) 16 | 17 | func checkOrCreateDefaults() { 18 | defaultsOnce.Do(func() { 19 | DefaultCommands = NewScopedCommands() 20 | }) 21 | } 22 | 23 | // AddCommand to the default command set. 24 | // See ScopedCommands' AddComand. 25 | func AddCommand(c Command) error { 26 | checkOrCreateDefaults() 27 | return DefaultCommands.AddCommand(c) 28 | } 29 | 30 | // AttachToStream if possible to start consuming the stream 31 | // and executing commands per the stored information in the ScopeCommands. 32 | func AttachToStream(ctx context.Context, input io.Reader, output io.Writer) { 33 | checkOrCreateDefaults() 34 | DefaultCommands.AttachToStream(ctx, input, output) 35 | } 36 | 37 | // AddDefaultsForScope for debugging. 38 | func AddDefaultsForScope(scopeID int32, controller interface{}) { 39 | checkOrCreateDefaults() 40 | if c, ok := controller.(window.Window); ok { 41 | DefaultCommands.AddDefaultsForScope(scopeID, c) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /debugstream/doc.go: -------------------------------------------------------------------------------- 1 | // Package debugstream provides an interface for custom debug commands to assist in 2 | // program development. 3 | package debugstream 4 | -------------------------------------------------------------------------------- /debugstream/mispellDetector_test.go: -------------------------------------------------------------------------------- 1 | package debugstream 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func Test_jaroDecreased(t *testing.T) { 9 | type args struct { 10 | candidate string 11 | registered string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want float64 17 | want2 float64 18 | }{ 19 | 20 | {"fullmatch", args{"super", "super"}, 1, 1}, 21 | {"partial by paper", args{"CRATE", "TRACE"}, 2.2 / 3.0, (2.2 / 3.0) * 1.2 * 1.05}, 22 | {"nomatch", args{"aaaaa", "super"}, 0, 0}, 23 | {"empty", args{"", "super"}, 0, 0}, 24 | 25 | {"partialex", args{"afulls", "fullscreen"}, 7.0 / 9.0, 1}, 26 | {"partialex2", args{"scope", "help"}, 3.0/20.0 + 1.0/3.0, (3.0/20.0 + 1.0/3.0) * 1.04}, 27 | {"low", args{"full", "help"}, 0.5, 0.5 * 1.04}, 28 | 29 | {"transposes", args{"ooftypething", "rooftypething"}, 0.97435897, 1}, 30 | 31 | {"single", args{"f", "fullscreen"}, (2.1) / 3.0, (2.1) / 3.0 * 1.1 * 1.1}, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | pseudoJ, psuedoJWithPref := jaroDecreased(tt.args.candidate, tt.args.registered) 36 | if floatSig(4, pseudoJ) != floatSig(4, tt.want) { 37 | t.Errorf("jaroDecreased of %s val = %v, wanted %v", tt.name, pseudoJ, tt.want) 38 | } 39 | if floatSig(4, psuedoJWithPref) != floatSig(4, tt.want2) { 40 | t.Errorf("jaroDecreased of %s with boost = %v, wanted %v", tt.name, psuedoJWithPref, tt.want2) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func floatSig(bits int, target float64) float64 { 47 | factor := math.Pow10(bits) 48 | return math.Floor(target*factor) / factor 49 | } 50 | -------------------------------------------------------------------------------- /debugtools/doc.go: -------------------------------------------------------------------------------- 1 | // Package debugtools provides structures to aid in development debugging of graphical programs. 2 | package debugtools 3 | -------------------------------------------------------------------------------- /debugtools/inputviz/controllerOutline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/debugtools/inputviz/controllerOutline.png -------------------------------------------------------------------------------- /debugtools/mouse.go: -------------------------------------------------------------------------------- 1 | package debugtools 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4/dlog" 5 | "github.com/oakmound/oak/v4/event" 6 | "github.com/oakmound/oak/v4/key" 7 | "github.com/oakmound/oak/v4/mouse" 8 | "github.com/oakmound/oak/v4/scene" 9 | ) 10 | 11 | // DebugMouseRelease will print the position and button pressed of the mouse when the mouse is released, if the given 12 | // key is held down at the time. If 0 is given, it will always be printed 13 | func DebugMouseRelease(ctx *scene.Context, k key.Code) { 14 | event.GlobalBind(ctx, mouse.Release, func(mev *mouse.Event) event.Response { 15 | if k == 0 || ctx.IsDown(k) { 16 | dlog.Info(mev) 17 | } 18 | return 0 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /debugtools/renderable.go: -------------------------------------------------------------------------------- 1 | package debugtools 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4/render" 5 | "golang.org/x/sync/syncmap" 6 | ) 7 | 8 | var ( 9 | debugMap syncmap.Map 10 | ) 11 | 12 | // SetDebugRenderable stores a renderable under a name in a package global map. 13 | // this is used by some built in debugConsole helper functions. 14 | func SetDebugRenderable(rName string, r render.Renderable) { 15 | debugMap.Store(rName, r) 16 | } 17 | 18 | // GetDebugRenderable returns whatever renderable is stored under the input 19 | // string, if any. 20 | func GetDebugRenderable(rName string) (render.Renderable, bool) { 21 | r, ok := debugMap.Load(rName) 22 | if r == nil { 23 | return nil, false 24 | } 25 | return r.(render.Renderable), ok 26 | } 27 | 28 | // EnumerateDebugRenderableKeys lists all registered renderables by key. 29 | // It does not check to see if the associated renderables are still valid in any respect. 30 | func EnumerateDebugRenderableKeys() []string { 31 | keys := []string{} 32 | debugMap.Range(func(k, v interface{}) bool { 33 | key, ok := k.(string) 34 | if ok { 35 | keys = append(keys, key) 36 | } 37 | return true 38 | }) 39 | return keys 40 | } 41 | -------------------------------------------------------------------------------- /default_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/alg/intgeom" 7 | "github.com/oakmound/oak/v4/key" 8 | "github.com/oakmound/oak/v4/render" 9 | "github.com/oakmound/oak/v4/scene" 10 | ) 11 | 12 | func TestDefaultFunctions(t *testing.T) { 13 | t.Run("SuperficialCoverage", func(t *testing.T) { 14 | IsDown(key.A) 15 | IsHeld(key.A) 16 | AddScene("test", scene.Scene{ 17 | Start: func(ctx *scene.Context) { 18 | ScreenShot() 19 | ctx.Window.Quit() 20 | }, 21 | }) 22 | SetViewportBounds(intgeom.NewRect2(0, 0, 1, 1)) 23 | SetViewport(intgeom.Point2{}) 24 | ShiftViewport(intgeom.Point2{}) 25 | UpdateViewSize(10, 10) 26 | Bounds() 27 | SetLoadingRenderable(render.EmptyRenderable()) 28 | SetColorBackground(nil) 29 | SetBackground(render.EmptyRenderable()) 30 | Init("test") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /dlog/default_test.go: -------------------------------------------------------------------------------- 1 | package dlog_test 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/oakmound/oak/v4/dlog" 9 | ) 10 | 11 | func TestLogger(t *testing.T) { 12 | lgr := dlog.NewLogger() 13 | 14 | err := lgr.SetLogLevel(-1) 15 | if err == nil { 16 | t.Fatalf("expected -1 log level to error") 17 | } 18 | 19 | lgr.SetLogLevel(dlog.VERBOSE) 20 | 21 | var buff = new(bytes.Buffer) 22 | 23 | lgr.SetOutput(buff) 24 | // This function wrapper corrects the logged file generated 25 | calllogger := func() { 26 | lgr.Error("error") 27 | lgr.Info("info") 28 | lgr.Verb("verb") 29 | 30 | lgr.SetFilter(func(s string) bool { return strings.Contains(s, "foo") }) 31 | lgr.Verb("bar") 32 | lgr.Verb("foo") 33 | } 34 | calllogger() 35 | 36 | out := buff.String() 37 | expected := []string{ 38 | "ERROR: error", 39 | "INFO: info", 40 | "VERBOSE: verb", 41 | "VERBOSE: foo", 42 | } 43 | 44 | lastIndexAt := 0 45 | for _, s := range expected { 46 | foundAt := strings.Index(out, s) 47 | if foundAt < lastIndexAt { 48 | t.Fatalf("did not find %v in correct order, was at index %v", s, foundAt) 49 | } 50 | lastIndexAt = foundAt 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dlog/doc.go: -------------------------------------------------------------------------------- 1 | // Package dlog provides basic logging functions. 2 | // 3 | // It is not intended to be a fully featured or fully optimized logger-- it is 4 | // just enough of a logger for oak's needs. A program utilizing oak, if it wants 5 | // more powerful logs, should log to a more powerful tool, and if desired, tell oak 6 | // to as well via setting dlog.DefaultLogger. 7 | package dlog 8 | -------------------------------------------------------------------------------- /dlog/levels.go: -------------------------------------------------------------------------------- 1 | package dlog 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/oakmound/oak/v4/oakerr" 7 | ) 8 | 9 | // Level represents the levels a debug message can have 10 | type Level int 11 | 12 | // Level values const 13 | const ( 14 | NONE Level = iota 15 | ERROR 16 | INFO 17 | VERBOSE 18 | ) 19 | 20 | var logLevels = map[Level]string{ 21 | NONE: "NONE", 22 | ERROR: "ERROR", 23 | INFO: "INFO", 24 | VERBOSE: "VERBOSE", 25 | } 26 | 27 | func (l Level) String() string { 28 | return logLevels[l] 29 | } 30 | 31 | // ParseDebugLevel parses the input string as a known debug levels 32 | func ParseDebugLevel(level string) (Level, error) { 33 | level = strings.ToUpper(level) 34 | switch level { 35 | case "INFO": 36 | return INFO, nil 37 | case "VERBOSE": 38 | return VERBOSE, nil 39 | case "ERROR": 40 | return ERROR, nil 41 | case "NONE": 42 | return NONE, nil 43 | default: 44 | return ERROR, oakerr.InvalidInput{InputName: "level"} 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dlog/levels_test.go: -------------------------------------------------------------------------------- 1 | package dlog_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/dlog" 7 | ) 8 | 9 | func TestLevelsString(t *testing.T) { 10 | type testCase struct { 11 | in dlog.Level 12 | out string 13 | } 14 | tcs := []testCase{ 15 | { 16 | in: dlog.NONE, 17 | out: "NONE", 18 | }, { 19 | in: dlog.ERROR, 20 | out: "ERROR", 21 | }, { 22 | in: dlog.INFO, 23 | out: "INFO", 24 | }, { 25 | in: dlog.VERBOSE, 26 | out: "VERBOSE", 27 | }, { 28 | in: dlog.Level(100), 29 | out: "", 30 | }, 31 | } 32 | for _, tc := range tcs { 33 | tc := tc 34 | t.Run(tc.out, func(t *testing.T) { 35 | out := tc.in.String() 36 | if out != tc.out { 37 | t.Fatalf("mismatched output, got %v expected %v", out, tc.out) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dlog/strings.go: -------------------------------------------------------------------------------- 1 | package dlog 2 | 3 | import "github.com/oakmound/oak/v4/oakerr" 4 | 5 | type logCode int 6 | 7 | // Constant log string identifiers. All log strings output by oak 8 | // should be enumerated here. 9 | const ( 10 | WindowClosed logCode = iota 11 | SceneStarting 12 | SceneLooping 13 | SceneEnding 14 | UnknownScene 15 | NoAudioDevice 16 | ) 17 | 18 | func (lc logCode) String() string { 19 | s := logstrings[oakerr.CurrentLanguage][lc] 20 | if s == "" { 21 | return logstrings[oakerr.ENG][lc] 22 | } 23 | return s 24 | } 25 | 26 | var logstrings = map[oakerr.Language]map[logCode]string{ 27 | oakerr.ENG: { 28 | WindowClosed: "Window closed", 29 | SceneStarting: "Scene start:", 30 | SceneLooping: "Looping scene", 31 | SceneEnding: "Scene end:", 32 | UnknownScene: "Unknown scene:", 33 | NoAudioDevice: "No default audio device available", 34 | }, 35 | oakerr.DEU: {}, 36 | oakerr.JPN: {}, 37 | } 38 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4/shiny/screen" 5 | ) 6 | 7 | // A Driver is a function which can take in our lifecycle function 8 | // and initialize oak with the OS interfaces it needs. 9 | type Driver func(f func(screen.Screen)) 10 | -------------------------------------------------------------------------------- /entities/doc.go: -------------------------------------------------------------------------------- 1 | // Package entities provides common entity constructor functions 2 | package entities 3 | -------------------------------------------------------------------------------- /entities/x/btn/grid/grid.go: -------------------------------------------------------------------------------- 1 | // Package grid provides structures for aligning grids of buttons 2 | package grid 3 | 4 | import ( 5 | "github.com/oakmound/oak/v4/entities" 6 | "github.com/oakmound/oak/v4/entities/x/btn" 7 | "github.com/oakmound/oak/v4/scene" 8 | ) 9 | 10 | // A Grid is a 2D slice of entities 11 | type Grid [][]*entities.Entity 12 | 13 | // A Generator defines the variables used to create grids from optional arguments 14 | type Generator struct { 15 | Content [][]btn.Option 16 | Defaults btn.Option 17 | XGap, YGap float64 18 | } 19 | 20 | var ( 21 | // A number of these fields could be removed, because they are the zero 22 | // value, but are left for documentation 23 | defaultGenerator = Generator{ 24 | Content: [][]btn.Option{ 25 | { 26 | nil, 27 | }, 28 | }, 29 | Defaults: nil, 30 | XGap: 0, 31 | YGap: 0, 32 | } 33 | ) 34 | 35 | // Generate creates a Grid from a Generator 36 | func (g *Generator) Generate(ctx *scene.Context) Grid { 37 | grid := make([][]*entities.Entity, len(g.Content)) 38 | for x := 0; x < len(g.Content); x++ { 39 | grid[x] = make([]*entities.Entity, len(g.Content[x])) 40 | for y := 0; y < len(g.Content[x]); y++ { 41 | grid[x][y] = btn.New(ctx, 42 | g.Defaults, 43 | g.Content[x][y], 44 | btn.Offset(float64(x)*g.XGap, float64(y)*g.YGap), 45 | ) 46 | } 47 | } 48 | return grid 49 | } 50 | 51 | // New creates a grid of buttons from a set of options 52 | func New(ctx *scene.Context, opts ...Option) Grid { 53 | g := defaultGenerator 54 | for _, opt := range opts { 55 | if opt == nil { 56 | continue 57 | } 58 | g = opt(g) 59 | } 60 | return g.Generate(ctx) 61 | } 62 | -------------------------------------------------------------------------------- /entities/x/btn/tree.go: -------------------------------------------------------------------------------- 1 | package btn 2 | 3 | // AddChildren adds a generator to create a child btn 4 | func AddChildren(cg ...Generator) Option { 5 | return func(g Generator) Generator { 6 | g.Children = append(g.Children, cg...) 7 | return g 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /entities/x/doc.go: -------------------------------------------------------------------------------- 1 | // Package x provides experimental utilities 2 | package x 3 | -------------------------------------------------------------------------------- /event/default.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | // DefaultBus is a global Bus. It uses the DefaultCallerMap internally. It should not be used unless your program is only 4 | // using a single Bus. Preferably multi-bus programs would create their own buses and caller maps specific to each bus's 5 | // use. 6 | var DefaultBus *Bus 7 | 8 | // DefaultCallerMap is a global CallerMap. It should not be used unless your program is only using a single CallerMap, 9 | // or in other words definitely only has one event bus running at a time. 10 | var DefaultCallerMap *CallerMap 11 | 12 | func init() { 13 | DefaultCallerMap = NewCallerMap() 14 | DefaultBus = NewBus(DefaultCallerMap) 15 | } 16 | -------------------------------------------------------------------------------- /event/doc.go: -------------------------------------------------------------------------------- 1 | // Package event provides structures to propagate event occurrences to subscribed system entities. 2 | package event 3 | -------------------------------------------------------------------------------- /event/events.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | // An UnsafeEventID is a non-typed eventID. EventIDs are just these, with type information attached. 9 | type UnsafeEventID int64 10 | 11 | // A EventID represents an event associated with a given payload type. 12 | type EventID[T any] struct { 13 | UnsafeEventID 14 | } 15 | 16 | var ( 17 | nextEventID int64 18 | ) 19 | 20 | // RegisterEvent returns a unique ID to associate an event with. EventIDs not created through RegisterEvent are 21 | // not valid for use in type-safe bindings. 22 | func RegisterEvent[T any]() EventID[T] { 23 | id := atomic.AddInt64(&nextEventID, 1) 24 | return EventID[T]{ 25 | UnsafeEventID: UnsafeEventID(id), 26 | } 27 | } 28 | 29 | // EnterPayload is the payload sent down to Enter bindings 30 | type EnterPayload struct { 31 | FramesElapsed int 32 | SinceLastFrame time.Duration 33 | TickPercent float64 34 | } 35 | 36 | var ( 37 | // Enter: the beginning of every logical frame. 38 | Enter = RegisterEvent[EnterPayload]() 39 | ) 40 | -------------------------------------------------------------------------------- /event/handler.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | var ( 4 | _ Handler = &Bus{} 5 | ) 6 | 7 | // Handler represents the necessary exported functions from an event.Bus 8 | // for use in oak internally, and thus the functions that need to be replaced 9 | // by alternative event handlers. 10 | type Handler interface { 11 | Reset() 12 | TriggerForCaller(cid CallerID, event UnsafeEventID, data interface{}) <-chan struct{} 13 | Trigger(event UnsafeEventID, data interface{}) <-chan struct{} 14 | UnsafeBind(UnsafeEventID, CallerID, UnsafeBindable) Binding 15 | Unbind(Binding) <-chan struct{} 16 | UnbindAllFrom(CallerID) <-chan struct{} 17 | SetCallerMap(*CallerMap) 18 | GetCallerMap() *CallerMap 19 | PersistentBind(eventID UnsafeEventID, callerID CallerID, fn UnsafeBindable) Binding 20 | ClearPersistentBindings() 21 | } 22 | -------------------------------------------------------------------------------- /event/internal.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type bindableList map[BindID]UnsafeBindable 8 | 9 | func (eb *Bus) getBindableList(eventID UnsafeEventID, callerID CallerID) bindableList { 10 | if m := eb.bindingMap[eventID]; m == nil { 11 | eb.bindingMap[eventID] = make(map[CallerID]bindableList) 12 | bl := make(bindableList) 13 | eb.bindingMap[eventID][callerID] = bl 14 | return bl 15 | } 16 | bl := eb.bindingMap[eventID][callerID] 17 | if bl == nil { 18 | bl = make(bindableList) 19 | eb.bindingMap[eventID][callerID] = bl 20 | } 21 | return bl 22 | } 23 | 24 | func (bus *Bus) trigger(binds bindableList, eventID UnsafeEventID, callerID CallerID, data interface{}) { 25 | wg := &sync.WaitGroup{} 26 | wg.Add(len(binds)) 27 | for bindID, bnd := range binds { 28 | bindID := bindID 29 | bnd := bnd 30 | go func() { 31 | if callerID == Global || bus.callerMap.HasEntity(callerID) { 32 | response := bnd(callerID, bus, data) 33 | switch response { 34 | case ResponseUnbindThisBinding: 35 | // Q: Why does this call bus.Unbind when it already has the event index to delete? 36 | // A: This goroutine does not own a write lock on the bus, and should therefore 37 | // not modify its contents. We do not have a simple way of promoting our read lock 38 | // to a write lock. 39 | bus.Unbind(Binding{EventID: eventID, CallerID: callerID, BindID: bindID, busResetCount: bus.resetCount}) 40 | case ResponseUnbindThisCaller: 41 | bus.UnbindAllFrom(callerID) 42 | } 43 | } 44 | wg.Done() 45 | }() 46 | } 47 | wg.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /event/response.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type Response uint8 4 | 5 | // Response types for bindables 6 | const ( 7 | // ResponseNone or 0, is returned by events that 8 | // don't want the event bus to do anything with 9 | // the event after they have been evaluated. This 10 | // is the usual behavior. 11 | ResponseNone Response = iota 12 | // ResponseUnbindThisBinding unbinds the one binding that returns it. 13 | ResponseUnbindThisBinding 14 | // ResponseUnbindThisCaller unbinds all of a caller's bindings when returned from any binding. 15 | ResponseUnbindThisCaller 16 | ) 17 | -------------------------------------------------------------------------------- /event/trigger.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | // TriggerForCaller acts like Trigger, but will only trigger for the given caller. 4 | func (bus *Bus) TriggerForCaller(callerID CallerID, eventID UnsafeEventID, data interface{}) <-chan struct{} { 5 | if callerID == Global { 6 | return bus.Trigger(eventID, data) 7 | } 8 | ch := make(chan struct{}) 9 | go func() { 10 | bus.mutex.RLock() 11 | if idMap, ok := bus.bindingMap[eventID]; ok { 12 | if bs, ok := idMap[callerID]; ok { 13 | bus.trigger(bs, eventID, callerID, data) 14 | } 15 | } 16 | bus.mutex.RUnlock() 17 | close(ch) 18 | }() 19 | return ch 20 | } 21 | 22 | // Trigger will scan through the event bus and call all bindables found attached 23 | // to the given event, with the passed in data. 24 | func (bus *Bus) Trigger(eventID UnsafeEventID, data interface{}) <-chan struct{} { 25 | ch := make(chan struct{}) 26 | go func() { 27 | bus.mutex.RLock() 28 | for callerID, bs := range bus.bindingMap[eventID] { 29 | bus.trigger(bs, eventID, callerID, data) 30 | } 31 | bus.mutex.RUnlock() 32 | close(ch) 33 | }() 34 | return ch 35 | } 36 | 37 | // TriggerOn calls Trigger with a strongly typed event. 38 | func TriggerOn[T any](b Handler, ev EventID[T], data T) <-chan struct{} { 39 | return b.Trigger(ev.UnsafeEventID, data) 40 | } 41 | 42 | // TriggerForCallerOn calls TriggerForCaller with a strongly typed event. 43 | func TriggerForCallerOn[T any](b Handler, cid CallerID, ev EventID[T], data T) <-chan struct{} { 44 | return b.TriggerForCaller(cid, ev.UnsafeEventID, data) 45 | } 46 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | See the following packages for larger examples than these snippets: 4 | 5 | - [A computational geometry / point location demo](https://github.com/200sc/go-compgeo/tree/master/demo) 6 | - [Jeremy the Clam, made for the Gopher Game Jam](https://github.com/200sc/jeremy) 7 | - [A Fantastic Doctor, made for the LOWREZJAM Game Jam](https://github.com/oakmound/lowrez17) 8 | - [Gel, a port of a C N64-esque model viewer](https://github.com/200sc/gel) 9 | -------------------------------------------------------------------------------- /examples/adventure/example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/adventure/example.PNG -------------------------------------------------------------------------------- /examples/bezier/README.md: -------------------------------------------------------------------------------- 1 | # Bezier Rendering 2 | Use a mouse or debug commands to create points. 3 | The points will be used to draw bézier curve. 4 | 5 | ![text](./example.PNG) -------------------------------------------------------------------------------- /examples/bezier/example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/bezier/example.PNG -------------------------------------------------------------------------------- /examples/blank/README.md: -------------------------------------------------------------------------------- 1 | # Blank Scene 2 | Starts a pprof server and sets up a blank scene. 3 | Useful for benchmarking and as a minimal base to copy from. 4 | For less minimalist copying point see [the basic game template](https://github.com/oakmound/game-template). -------------------------------------------------------------------------------- /examples/blank/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | _ "net/http/pprof" 7 | 8 | "github.com/oakmound/oak/v4" 9 | "github.com/oakmound/oak/v4/render" 10 | "github.com/oakmound/oak/v4/scene" 11 | ) 12 | 13 | // This example is a blank, default scene with a pprof server. Useful for 14 | // benchmarks and as a base to copy a starting point from. 15 | 16 | func main() { 17 | go func() { 18 | log.Println(http.ListenAndServe("localhost:6060", nil)) 19 | }() 20 | oak.AddScene("blank", scene.Scene{ 21 | Start: func(ctx *scene.Context) { 22 | ctx.DrawStack.Draw(render.NewDrawFPS(0, nil, 10, 10)) 23 | ctx.DrawStack.Draw(render.NewLogicFPS(0, nil, 10, 20)) 24 | }, 25 | }) 26 | oak.Init("blank") 27 | } 28 | -------------------------------------------------------------------------------- /examples/collision/README.md: -------------------------------------------------------------------------------- 1 | # Collision Demo 2 | 3 | Controllable box that colors itself based on what zones it collides with. 4 | 5 | ![text](./example.PNG) -------------------------------------------------------------------------------- /examples/flappy-bird/README.md: -------------------------------------------------------------------------------- 1 | # Flappy Bird 2 | A simple implementation of Flappy Bird 3 | 4 | ![text](./example.PNG) -------------------------------------------------------------------------------- /examples/flappy-bird/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/flappy-bird/example.gif -------------------------------------------------------------------------------- /examples/joystick-viz/README.md: -------------------------------------------------------------------------------- 1 | # Joystick Demo 2 | 3 | Plugin a joystick and see a representation pop up on screen. 4 | 5 | There is a bit more code here but it mainly relates to setting up the assets to correctly show up on the joystick. 6 | 7 | ![text](./example.gif) -------------------------------------------------------------------------------- /examples/joystick-viz/controllerOutline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/joystick-viz/controllerOutline.png -------------------------------------------------------------------------------- /examples/joystick-viz/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/joystick-viz/example.gif -------------------------------------------------------------------------------- /examples/js-server/README.md: -------------------------------------------------------------------------------- 1 | # Example JS server 2 | 3 | The script in this directory serves examples as simple wasm embedded canvases. 4 | 5 | To use, `go run main.go` with an optional `-port` flag and navigate to `localhost:8080/`. 6 | 7 | If the directory has no main.wasm file created to embed, the commands to generate that file will be presented. 8 | 9 | -------------------------------------------------------------------------------- /examples/js-server/index.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | Go wasm 12 | 13 | 14 | 15 | 20 | 21 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/keyboard-viz/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | 8 | "github.com/oakmound/oak/v4" 9 | "github.com/oakmound/oak/v4/alg/floatgeom" 10 | "github.com/oakmound/oak/v4/debugtools/inputviz" 11 | "github.com/oakmound/oak/v4/dlog" 12 | "github.com/oakmound/oak/v4/render" 13 | "github.com/oakmound/oak/v4/scene" 14 | ) 15 | 16 | func main() { 17 | oak.AddScene("keyviz", scene.Scene{ 18 | Start: func(ctx *scene.Context) { 19 | fmt.Println("start") 20 | fnt, _ := render.DefFontGenerator.RegenerateWith(func(fg render.FontGenerator) render.FontGenerator { 21 | fg.Color = image.NewUniform(color.RGBA{0, 0, 0, 255}) 22 | fg.Size = 13 23 | return fg 24 | }) 25 | bds := ctx.Window.Bounds() 26 | m := inputviz.Keyboard{ 27 | Rect: floatgeom.NewRect2(0, 0, float64(bds.X()), float64(bds.Y())), 28 | BaseLayer: -1, 29 | RenderCharacters: true, 30 | Font: fnt, 31 | } 32 | m.RenderAndListen(ctx, 0) 33 | }, 34 | }) 35 | err := oak.Init("keyviz", func(c oak.Config) (oak.Config, error) { 36 | c.Debug.Level = dlog.VERBOSE.String() 37 | c.Screen.Width = 800 38 | c.Screen.Height = 300 39 | return c, nil 40 | }) 41 | if err != nil { 42 | fmt.Println(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/mouse-viz/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4" 5 | "github.com/oakmound/oak/v4/alg/floatgeom" 6 | "github.com/oakmound/oak/v4/debugtools/inputviz" 7 | "github.com/oakmound/oak/v4/scene" 8 | ) 9 | 10 | func main() { 11 | oak.AddScene("mouseviz", scene.Scene{ 12 | Start: func(ctx *scene.Context) { 13 | bds := ctx.Window.Bounds() 14 | m := inputviz.Mouse{ 15 | Rect: floatgeom.NewRect2(0, 0, float64(bds.X()), float64(bds.Y())), 16 | BaseLayer: -1, 17 | } 18 | m.RenderAndListen(ctx, 0) 19 | }, 20 | }) 21 | oak.Init("mouseviz", func(c oak.Config) (oak.Config, error) { 22 | c.Screen.Width = 100 23 | c.Screen.Height = 140 24 | return c, nil 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /examples/multi-window/README.md: -------------------------------------------------------------------------------- 1 | # Multi Window 2 | An example of managing multiple windows. 3 | 4 | ![text](./example.PNG) -------------------------------------------------------------------------------- /examples/multi-window/example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/multi-window/example.PNG -------------------------------------------------------------------------------- /examples/particle-demo/README.md: -------------------------------------------------------------------------------- 1 | # Particles 2 | 3 | Play around with a particle source. Checkout the different options you have to play around with. 4 | 5 | Change where the particels are coming from, their size, where they are being pulled to and much more! 6 | 7 | ![text](./example.gif) -------------------------------------------------------------------------------- /examples/particle-demo/overviewExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/particle-demo/overviewExample.gif -------------------------------------------------------------------------------- /examples/piano/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/piano/example.gif -------------------------------------------------------------------------------- /examples/platformer/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/platformer/example.gif -------------------------------------------------------------------------------- /examples/pong/README.md: -------------------------------------------------------------------------------- 1 | # Pong 2 | 3 | Simple Pong implementation 4 | 5 | 6 | ![text](./example.gif) -------------------------------------------------------------------------------- /examples/rooms/example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/rooms/example.PNG -------------------------------------------------------------------------------- /examples/screenopts/example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/screenopts/example.PNG -------------------------------------------------------------------------------- /examples/sprite/README.md: -------------------------------------------------------------------------------- 1 | # Sprite demo 2 | 3 | Hold down k to have more gophers bouncing around! 4 | 5 | ![text](./example.gif) 6 | 7 | 8 | 9 | 10 | 11 | This demo is designed to mirror [this ebiten demo](https://hajimehoshi.github.io/ebiten/examples/sprites.html). This demo performs worse than ebiten, because oak does not as of writing use openGL, and because ebiten's screen size is a quarter of oak's. 12 | 13 | The gopher image used is from [gophericons](https://github.com/shalakhin/gophericons), based on Renee French under Creative Commons 3.0 Attributions. -------------------------------------------------------------------------------- /examples/sprite/assets/images/raw/gopher11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/sprite/assets/images/raw/gopher11.png -------------------------------------------------------------------------------- /examples/text/README.md: -------------------------------------------------------------------------------- 1 | # Text Creation 2 | 3 | Examples of drawing text, supporting multiple fonts, and changing text color / content while on the screen. 4 | -------------------------------------------------------------------------------- /examples/text/assets/font/luxisbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/text/assets/font/luxisbi.ttf -------------------------------------------------------------------------------- /examples/text/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oakmound/oak/examples/text-demo 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/flopp/go-findfont v0.0.0-20201114153133-e7393a00c15b 7 | github.com/oakmound/oak/v4 v4.1.1 8 | ) 9 | 10 | require ( 11 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20221208032759-85de2813cf6b // indirect 12 | github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect 13 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect 14 | github.com/disintegration/gift v1.2.1 // indirect 15 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // indirect 16 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 17 | github.com/jfreymuth/pulse v0.1.0 // indirect 18 | github.com/oakmound/alsa v0.0.2 // indirect 19 | github.com/oakmound/libudev v0.2.1 // indirect 20 | github.com/oakmound/w32 v2.1.0+incompatible // indirect 21 | github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // indirect 22 | golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd // indirect 23 | golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf // indirect 24 | golang.org/x/image v0.21.0 // indirect 25 | golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 // indirect 26 | golang.org/x/sync v0.1.0 // indirect 27 | golang.org/x/sys v0.29.0 // indirect 28 | ) 29 | 30 | replace github.com/oakmound/oak/v4 => ../.. 31 | -------------------------------------------------------------------------------- /examples/top-down-shooter/assets/images/16x16/sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/top-down-shooter/assets/images/16x16/sheet.png -------------------------------------------------------------------------------- /examples/top-down-shooter/assets/images/character/eggplant-fish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/top-down-shooter/assets/images/character/eggplant-fish.png -------------------------------------------------------------------------------- /examples/top-down-shooter/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/examples/top-down-shooter/example.gif -------------------------------------------------------------------------------- /fileutil/doc.go: -------------------------------------------------------------------------------- 1 | // Package fileutil provides functionality to replace os and io calls with custom filesystems. 2 | // It is a small wrapper around io/fs. 3 | package fileutil 4 | -------------------------------------------------------------------------------- /fileutil/os_fallback_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package fileutil 5 | 6 | func init() { 7 | // OS calls always fall in JS, disable calling to it by default 8 | OSFallback = false 9 | } -------------------------------------------------------------------------------- /fileutil/testdata/test.txt: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oakmound/oak/v4 2 | 3 | go 1.18 4 | 5 | require ( 6 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20221208032759-85de2813cf6b // osx, shiny 7 | github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // linux, shiny 8 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // linux, shiny 9 | github.com/disintegration/gift v1.2.1 // render 10 | github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 11 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // osx, shiny 12 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 13 | github.com/hajimehoshi/go-mp3 v0.3.2 14 | github.com/jfreymuth/pulse v0.1.0 // linux, audio 15 | github.com/oakmound/alsa v0.0.2 // linux, audio 16 | github.com/oakmound/libudev v0.2.1 // linux, joystick 17 | github.com/oakmound/w32 v2.1.0+incompatible // windows, shiny 18 | github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // windows, audio 19 | golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd 20 | golang.org/x/image v0.21.0 21 | golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 22 | golang.org/x/sync v0.1.0 23 | golang.org/x/sys v0.5.0 24 | ) 25 | 26 | require ( 27 | github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d // indirect 28 | golang.org/x/exp/shiny v0.0.0-20220518171630-0b5c67f07fdf // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /init_override_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package oak 5 | 6 | import ( 7 | "github.com/oakmound/oak/v4/dlog" 8 | "syscall/js" 9 | ) 10 | 11 | func overrideInit(w *Window) { 12 | w.DrawTicker.Stop() 13 | if w.DrawFrameRate != 60 { 14 | dlog.Info("Ignoring draw frame rate in JS") 15 | } 16 | if w.config.EnableDebugConsole { 17 | dlog.Info("Debug console is not supported in JS") 18 | w.config.EnableDebugConsole = false 19 | } 20 | if w.config.UnlimitedDrawFrameRate { 21 | dlog.Info("Unlimited draw frame rate is not supported in JS") 22 | w.config.UnlimitedDrawFrameRate = false 23 | } 24 | w.animationFrame = make(chan struct{}) 25 | js.Global().Call("requestAnimationFrame", js.FuncOf(w.requestFrame)) 26 | } 27 | 28 | func (w *Window) requestFrame(this js.Value, args []js.Value) interface{} { 29 | w.animationFrame <- struct{}{} 30 | js.Global().Call("requestAnimationFrame", js.FuncOf(w.requestFrame)) 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /init_override_other.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package oak 5 | 6 | func overrideInit(w *Window) {} -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestInitFailures(t *testing.T) { 9 | t.Run("BadConfig", func(t *testing.T) { 10 | c1 := NewWindow() 11 | err := c1.Init("", func(c Config) (Config, error) { 12 | return c, fmt.Errorf("whoops") 13 | }) 14 | if err == nil { 15 | t.Fatal("expected error to cascade down from init") 16 | } 17 | }) 18 | t.Run("ParseDebugLevel", func(t *testing.T) { 19 | c1 := NewWindow() 20 | err := c1.Init("", func(c Config) (Config, error) { 21 | c.Debug.Level = "bogus" 22 | return c, nil 23 | }) 24 | if err == nil { 25 | t.Fatal("expected error parsing debug level") 26 | } 27 | }) 28 | t.Run("SetLanguageString", func(t *testing.T) { 29 | c1 := NewWindow() 30 | err := c1.Init("", func(c Config) (Config, error) { 31 | c.Language = "bogus" 32 | return c, nil 33 | }) 34 | if err == nil { 35 | t.Fatal("expected error parsing language string") 36 | } 37 | }) 38 | } 39 | 40 | func TestInitDebugConsole(t *testing.T) { 41 | c1 := NewWindow() 42 | c1.Init("bad", func(c Config) (Config, error) { 43 | c.EnableDebugConsole = true 44 | return c, nil 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /inputLoop_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/oakmound/oak/v4/event" 8 | "golang.org/x/mobile/event/key" 9 | "golang.org/x/mobile/event/mouse" 10 | ) 11 | 12 | func TestInputLoop(t *testing.T) { 13 | c1 := blankScene(t) 14 | c1.SetLogicHandler(event.NewBus(nil)) 15 | c1.Window.Send(key.Event{ 16 | Direction: key.DirPress, 17 | Code: key.Code0, 18 | }) 19 | c1.Window.Send(key.Event{ 20 | Direction: key.DirNone, 21 | Code: key.Code0, 22 | }) 23 | c1.Window.Send(key.Event{ 24 | Direction: key.DirRelease, 25 | Code: key.Code0, 26 | }) 27 | c1.Window.Send(mouse.Event{}) 28 | time.Sleep(2 * time.Second) 29 | } 30 | -------------------------------------------------------------------------------- /joystick/driver_darwin.go: -------------------------------------------------------------------------------- 1 | package joystick 2 | 3 | import "github.com/oakmound/oak/v4/oakerr" 4 | 5 | func osinit() error { 6 | return nil 7 | } 8 | 9 | func newOsJoystick() osJoystick { 10 | return osJoystick{} 11 | } 12 | 13 | type osJoystick struct { 14 | } 15 | 16 | func (j *Joystick) prepare() error { 17 | return oakerr.UnsupportedPlatform{Operation: "joystick"} 18 | } 19 | 20 | func (j *Joystick) getState() (*State, error) { 21 | return nil, oakerr.UnsupportedPlatform{Operation: "joystick"} 22 | } 23 | 24 | func (j *Joystick) vibrate(left, right uint16) error { 25 | return oakerr.UnsupportedPlatform{Operation: "joystick"} 26 | } 27 | 28 | func (j *Joystick) close() error { 29 | return oakerr.UnsupportedPlatform{Operation: "joystick"} 30 | } 31 | 32 | func getJoysticks() []*Joystick { 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /joystick/driver_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !linux && !darwin && !js 2 | // +build !windows,!linux,!darwin,!js 3 | 4 | package joystick 5 | 6 | import "github.com/oakmound/oak/v4/oakerr" 7 | 8 | func newOsJoystick() osJoystick { 9 | return osJoystick{} 10 | } 11 | 12 | type osJoystick struct { 13 | } 14 | 15 | func osinit() error { 16 | return nil 17 | } 18 | 19 | func (j *Joystick) prepare() error { 20 | return oakerr.UnsupportedPlatform{Operation: "joystick"} 21 | } 22 | 23 | func (j *Joystick) getState() (*State, error) { 24 | return nil, oakerr.UnsupportedPlatform{Operation: "joystick"} 25 | } 26 | 27 | func (j *Joystick) vibrate(left, right uint16) error { 28 | return oakerr.UnsupportedPlatform{Operation: "joystick"} 29 | } 30 | 31 | func (j *Joystick) close() error { 32 | return oakerr.UnsupportedPlatform{Operation: "joystick"} 33 | } 34 | 35 | func getJoysticks() []*Joystick { 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /key/doc.go: -------------------------------------------------------------------------------- 1 | // Package key enumerates keystrings for use in bindings. 2 | package key 3 | -------------------------------------------------------------------------------- /key/state_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestState(t *testing.T) { 9 | ks := NewState() 10 | ks.SetDown(A) 11 | if !ks.IsDown(A) { 12 | t.Fatalf("a was not set down") 13 | } 14 | ks.SetUp(A) 15 | if ks.IsDown(A) { 16 | t.Fatalf("a was not set up") 17 | } 18 | ks.SetDown(A) 19 | time.Sleep(2 * time.Second) 20 | ok, d := ks.IsHeld(A) 21 | if !ok { 22 | t.Fatalf("a was not held down") 23 | } 24 | if d < 2000*time.Millisecond { 25 | t.Fatalf("a was not held down for sleep length") 26 | } 27 | ks.SetUp(A) 28 | ok, d = ks.IsHeld(A) 29 | if ok { 30 | t.Fatalf("a was not released") 31 | } 32 | if d != 0 { 33 | t.Fatalf("a hold was not reset") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lifecycle_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAspectRatio(t *testing.T) { 8 | c1 := blankScene(t) 9 | c1.SetAspectRatio(2) 10 | c1.ChangeWindow(10, 10) 11 | w := c1.windowRect.Max.X - c1.windowRect.Min.X 12 | h := c1.windowRect.Max.Y - c1.windowRect.Min.Y 13 | if w != 10 { 14 | t.Fatalf("height was not 10, got %v", w) 15 | } 16 | if h != 5 { 17 | t.Fatalf("height was not 5, got %v", h) 18 | } 19 | c1.ChangeWindow(10, 2) 20 | w = c1.windowRect.Max.X - c1.windowRect.Min.X 21 | h = c1.windowRect.Max.Y - c1.windowRect.Min.Y 22 | if w != 4 { 23 | t.Fatalf("height was not 4, got %v", w) 24 | } 25 | if h != 2 { 26 | t.Fatalf("height was not 2, got %v", h) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /loading.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "io/fs" 5 | 6 | "github.com/oakmound/oak/v4/audio" 7 | "github.com/oakmound/oak/v4/dlog" 8 | "github.com/oakmound/oak/v4/fileutil" 9 | "github.com/oakmound/oak/v4/render" 10 | "golang.org/x/sync/errgroup" 11 | ) 12 | 13 | func (w *Window) loadAssets(imageDir, audioDir string) { 14 | var eg errgroup.Group 15 | eg.Go(func() error { 16 | err := render.BlankBatchLoad(imageDir, w.config.BatchLoadOptions.MaxImageFileSize) 17 | if err != nil { 18 | return err 19 | } 20 | dlog.Verb("Done Loading Images") 21 | return nil 22 | }) 23 | eg.Go(func() error { 24 | var err error 25 | if w.config.BatchLoadOptions.BlankOutAudio { 26 | err = audio.BlankBatchLoad(audioDir) 27 | } else { 28 | err = audio.BatchLoad(audioDir) 29 | } 30 | dlog.Verb("Done Loading Audio") 31 | return err 32 | }) 33 | dlog.ErrorCheck(eg.Wait()) 34 | } 35 | 36 | func (w *Window) endLoad() { 37 | dlog.Verb("Done Loading") 38 | w.NextScene() 39 | } 40 | 41 | // SetFS updates all calls oak or oak's subpackages will make to read from the given filesystem. 42 | // By default, this is set to os.DirFS(".") 43 | func SetFS(filesystem fs.FS) { 44 | fileutil.FS = filesystem 45 | } 46 | -------------------------------------------------------------------------------- /loading_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/oakmound/oak/v4/scene" 8 | ) 9 | 10 | func TestBatchLoad_HappyPath(t *testing.T) { 11 | c1 := NewWindow() 12 | c1.AddScene("1", scene.Scene{ 13 | Start: func(context *scene.Context) { 14 | context.Window.Quit() 15 | }, 16 | }) 17 | c1.Init("1", func(c Config) (Config, error) { 18 | c.BatchLoad = true 19 | c.Assets.AudioPath = "testdata/audio" 20 | c.Assets.ImagePath = "testdata/images" 21 | return c, nil 22 | }) 23 | } 24 | 25 | func TestBatchLoad_NotFound(t *testing.T) { 26 | c1 := NewWindow() 27 | c1.AddScene("1", scene.Scene{ 28 | Start: func(context *scene.Context) { 29 | context.Window.Quit() 30 | }, 31 | }) 32 | c1.Init("1", func(c Config) (Config, error) { 33 | c.BatchLoad = true 34 | return c, nil 35 | }) 36 | } 37 | 38 | func TestBatchLoad_Blank(t *testing.T) { 39 | c1 := NewWindow() 40 | c1.AddScene("1", scene.Scene{ 41 | Start: func(context *scene.Context) { 42 | context.Window.Quit() 43 | }, 44 | }) 45 | c1.Init("1", func(c Config) (Config, error) { 46 | c.BatchLoad = true 47 | c.BatchLoadOptions.BlankOutAudio = true 48 | return c, nil 49 | }) 50 | } 51 | 52 | func TestSetBinaryPayload(t *testing.T) { 53 | // coverage test, this utility is effectively tested in the render package 54 | SetFS(os.DirFS(".")) 55 | } 56 | -------------------------------------------------------------------------------- /mouse/default_test.go: -------------------------------------------------------------------------------- 1 | package mouse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/collision" 7 | ) 8 | 9 | func TestDefaultFunctions(t *testing.T) { 10 | Clear() 11 | s := collision.NewUnassignedSpace(0, 0, 10, 10) 12 | Add(s) 13 | Remove(s) 14 | if len(Hits(collision.NewUnassignedSpace(1, 1, 1, 1))) != 0 { 15 | t.Fatalf("expected empty tree to have no contents") 16 | } 17 | 18 | Add(s) 19 | if ShiftSpace(3, 3, s) != nil { 20 | t.Fatalf("shift space failed") 21 | } 22 | if len(Hits(collision.NewUnassignedSpace(1, 1, 1, 1))) != 0 { 23 | t.Fatalf("hit away from space should not collide with space") 24 | } 25 | 26 | if UpdateSpace(0, 0, 10, 10, s) != nil { 27 | t.Fatalf("update space failed") 28 | } 29 | if len(Hits(collision.NewUnassignedSpace(1, 1, 1, 1))) == 0 { 30 | t.Fatalf("hit on space should collide") 31 | } 32 | 33 | Clear() 34 | if len(Hits(collision.NewUnassignedSpace(1, 1, 1, 1))) != 0 { 35 | t.Fatalf("expected cleared tree to have no contents") 36 | } 37 | 38 | s = collision.NewLabeledSpace(0, 0, 10, 10, collision.Label(2)) 39 | Add(s) 40 | if HitLabel(collision.NewUnassignedSpace(1, 1, 1, 1), collision.Label(2)) == nil { 41 | t.Fatalf("hit label missed") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mouse/doc.go: -------------------------------------------------------------------------------- 1 | // Package mouse handles the propagation of mouse events 2 | // though clickable regions. It extends the functionality 3 | // of the collision package. 4 | package mouse 5 | -------------------------------------------------------------------------------- /mouse/event_test.go: -------------------------------------------------------------------------------- 1 | package mouse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/collision" 7 | ) 8 | 9 | func TestEventConversions(t *testing.T) { 10 | me := NewEvent(1.0, 1.0, ButtonLeft, Drag) 11 | s := me.ToSpace() 12 | Add(collision.NewUnassignedSpace(1.0, 1.0, .1, .1)) 13 | if len(Hits(s)) == 0 { 14 | t.Fatalf("expected hits to catch unassigned space") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mouse/mouse.go: -------------------------------------------------------------------------------- 1 | package mouse 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4/event" 5 | "golang.org/x/mobile/event/mouse" 6 | ) 7 | 8 | // Button represents a mouse interaction type, like a left button or 9 | // mouse wheel movement. 10 | type Button = mouse.Button 11 | 12 | // Valid Button event types 13 | const ( 14 | ButtonLeft = mouse.ButtonLeft 15 | ButtonMiddle = mouse.ButtonMiddle 16 | ButtonRight = mouse.ButtonRight 17 | ButtonWheelDown = mouse.ButtonWheelDown 18 | ButtonWheelUp = mouse.ButtonWheelUp 19 | ButtonWheelLeft = mouse.ButtonWheelLeft 20 | ButtonWheelRight = mouse.ButtonWheelRight 21 | ButtonNone = mouse.ButtonNone 22 | ) 23 | 24 | // GetEventName returns a string event name given some mobile/mouse information 25 | func GetEvent(d mouse.Direction, b mouse.Button) event.EventID[*Event] { 26 | switch d { 27 | case mouse.DirPress: 28 | return Press 29 | case mouse.DirRelease: 30 | return Release 31 | default: 32 | switch b { 33 | case -2: 34 | return ScrollDown 35 | case -1: 36 | return ScrollUp 37 | } 38 | } 39 | return Drag 40 | } 41 | -------------------------------------------------------------------------------- /mouse/mouse_test.go: -------------------------------------------------------------------------------- 1 | package mouse 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/mobile/event/mouse" 7 | ) 8 | 9 | func TestEventNameIdentity(t *testing.T) { 10 | if GetEvent(mouse.DirPress, 0) != Press { 11 | t.Fatalf("event mismatch for event %v, expected %v", mouse.DirPress, "MousePress") 12 | } 13 | if GetEvent(mouse.DirRelease, 0) != Release { 14 | t.Fatalf("event mismatch for event %v, expected %v", mouse.DirRelease, "MouseRelease") 15 | } 16 | if GetEvent(mouse.DirNone, -2) != ScrollDown { 17 | t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollDown") 18 | } 19 | if GetEvent(mouse.DirNone, -1) != ScrollUp { 20 | t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseScrollUp") 21 | } 22 | if GetEvent(mouse.DirNone, 0) != Drag { 23 | t.Fatalf("event mismatch for event %v, expected %v", mouse.DirNone, "MouseDrag") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /oakerr/doc.go: -------------------------------------------------------------------------------- 1 | // Package oakerr stores errors returned throughout oak. 2 | package oakerr 3 | -------------------------------------------------------------------------------- /oakerr/errors_test.go: -------------------------------------------------------------------------------- 1 | package oakerr 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestErrorsAreErrors(t *testing.T) { 8 | languages := []Language{ENG, DEU} 9 | for _, lang := range languages { 10 | CurrentLanguage = lang 11 | var err error = NotFound{} 12 | if err.Error() == "" { 13 | t.Fatalf("NotFound error was empty") 14 | } 15 | err = ExistingElement{} 16 | if err.Error() == "" { 17 | t.Fatalf("ExistingElement error was empty") 18 | } 19 | err = ExistingElement{Overwritten: true} 20 | if err.Error() == "" { 21 | t.Fatalf("ExistingElement error was empty") 22 | } 23 | err = InsufficientInputs{} 24 | if err.Error() == "" { 25 | t.Fatalf("InsufficientInputs error was empty") 26 | } 27 | err = InvalidInput{} 28 | if err.Error() == "" { 29 | t.Fatalf("InvalidInput error was empty") 30 | } 31 | err = NilInput{} 32 | if err.Error() == "" { 33 | t.Fatalf("NilInput error was empty") 34 | } 35 | err = IndivisibleInput{} 36 | if err.Error() == "" { 37 | t.Fatalf("IndivisibleInput error was empty") 38 | } 39 | err = UnsupportedFormat{} 40 | if err.Error() == "" { 41 | t.Fatalf("UnsupportedFormat error was empty") 42 | } 43 | err = UnsupportedPlatform{} 44 | if err.Error() == "" { 45 | t.Fatalf("UnsupportedPlatform error was empty") 46 | } 47 | } 48 | // Assert nothing crashed 49 | } 50 | 51 | func TestErrorFallback(t *testing.T) { 52 | CurrentLanguage = JPN 53 | s := errorString(codeIndivisibleInput, "a", "b") 54 | if s != "a was not divisible by b" { 55 | t.Fatalf("language fallback to english failed") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /oakerr/language.go: -------------------------------------------------------------------------------- 1 | package oakerr 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Language configures the language of returned error strings 8 | type Language int 9 | 10 | var ( 11 | // CurrentLanguage is the current language for error and log strings 12 | CurrentLanguage Language 13 | ) 14 | 15 | // SetLanguageString parses a string as a language 16 | func SetLanguageString(language string) error { 17 | language = strings.ToUpper(language) 18 | switch language { 19 | case "EN", "ENGLISH": 20 | CurrentLanguage = ENG 21 | case "DE", "GERMAN", "DEUTSCH": 22 | CurrentLanguage = DEU 23 | case "JP", "JAPANESE", "日本語": 24 | CurrentLanguage = JPN 25 | default: 26 | return InvalidInput{InputName: language} 27 | } 28 | return nil 29 | } 30 | 31 | // Valid languages, uppercase ISO 639-2 32 | const ( 33 | // English 34 | ENG Language = iota 35 | // German 36 | DEU 37 | // Japanese 38 | JPN 39 | ) 40 | 41 | // Q: Why these languages? 42 | // A: These are the languages I (200sc) know or am actively learning 43 | -------------------------------------------------------------------------------- /oakerr/language_test.go: -------------------------------------------------------------------------------- 1 | package oakerr 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSetLanguageString(t *testing.T) { 8 | err := SetLanguageString("Gibberish") 9 | if err == nil { 10 | t.Fatal("Setting to language Gibberish did not error") 11 | } 12 | err = SetLanguageString("German") 13 | if err != nil { 14 | t.Fatalf("SetLanguageString failed: %v", err) 15 | } 16 | if CurrentLanguage != DEU { 17 | t.Fatalf("German did not set language to Deutsch") 18 | } 19 | err = SetLanguageString("English") 20 | if err != nil { 21 | t.Fatalf("SetLanguageString failed: %v", err) 22 | } 23 | if CurrentLanguage != ENG { 24 | t.Fatalf("English did not set language to English") 25 | } 26 | err = SetLanguageString("Japanese") 27 | if err != nil { 28 | t.Fatalf("SetLanguageString failed: %v", err) 29 | } 30 | if CurrentLanguage != JPN { 31 | t.Fatalf("Japanese did not set language to 日本語") 32 | } 33 | err = SetLanguageString("日本語") 34 | if err != nil { 35 | t.Fatalf("SetLanguageString failed: %v", err) 36 | } 37 | if CurrentLanguage != JPN { 38 | t.Fatalf("日本語 did not set language to 日本語") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /physics/attach.go: -------------------------------------------------------------------------------- 1 | package physics 2 | 3 | // An Attachable can be attached to static or moving vectors. 4 | type Attachable interface { 5 | Detach() 6 | Attach(Vecer, float64, float64) 7 | AttachX(Vecer, float64) 8 | AttachY(Vecer, float64) 9 | Vecer 10 | } 11 | 12 | // A Vecer can be converted into a Vector 13 | type Vecer interface { 14 | Vec() Vector 15 | } 16 | 17 | // Vec returns a vector itself 18 | func (v Vector) Vec() Vector { 19 | return v 20 | } 21 | 22 | // Attach takes in something for this vector to attach to and a set of 23 | // offsets. 24 | func (v *Vector) Attach(a Vecer, offX, offY float64) { 25 | v2 := a.Vec() 26 | v.x = v2.x 27 | v.y = v2.y 28 | v.offX = offX 29 | v.offY = offY 30 | } 31 | 32 | // AttachX performs an attachment that only attaches on the X axis. 33 | func (v *Vector) AttachX(a Vecer, offX float64) { 34 | v2 := a.Vec() 35 | v.x = v2.x 36 | v.offX = offX 37 | } 38 | 39 | // AttachY performs an attachment that only attaches on the Y axis. 40 | func (v *Vector) AttachY(a Vecer, offY float64) { 41 | v2 := a.Vec() 42 | v.y = v2.y 43 | v.offY = offY 44 | } 45 | 46 | // Detach modifies a vector to no longer be attached to anything. 47 | func (v *Vector) Detach() { 48 | v2 := NewVector(v.X(), v.Y()) 49 | *v = v2 50 | } 51 | 52 | // DetachX modifies a vector to no longer be attached on the X Axis. 53 | func (v *Vector) DetachX() { 54 | x := v.X() 55 | v.x = &x 56 | v.offX = 0 57 | } 58 | 59 | // DetachY modifies a vector to no longer be attached on the Y Axis. 60 | func (v *Vector) DetachY() { 61 | y := v.Y() 62 | v.y = &y 63 | v.offY = 0 64 | } 65 | -------------------------------------------------------------------------------- /physics/doc.go: -------------------------------------------------------------------------------- 1 | // Package physics provides vector types and trivial physics manipulation for them. 2 | package physics 3 | -------------------------------------------------------------------------------- /physics/force_test.go: -------------------------------------------------------------------------------- 1 | package physics 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type DeltaMass struct { 8 | Mass 9 | delta Vector 10 | } 11 | 12 | func (dm *DeltaMass) GetDelta() Vector { 13 | return dm.delta 14 | } 15 | 16 | func TestForce(t *testing.T) { 17 | v := NewForceVector(NewVector(100, 0), 100) 18 | v2 := DefaultForceVector(NewVector(100, 0), 100) 19 | if *v.Force != 100.0 { 20 | t.Fatalf("force vector created with 100 force did not have 100 force") 21 | } 22 | if *v2.Force != 10000.0 { 23 | t.Fatalf("mass to force vector did not scale") 24 | } 25 | 26 | v3 := NewVector(100, 100).GetForce() 27 | if *v3.Force != 0.0 { 28 | t.Fatalf("Non-force vector had force") 29 | } 30 | 31 | dm := &DeltaMass{ 32 | Mass{100}, 33 | NewVector(100, 0), 34 | } 35 | 36 | if dm.SetMass(-10) == nil { 37 | t.Fatalf("set mass to negative did not fail") 38 | } 39 | if dm.SetMass(10) != nil { 40 | t.Fatalf("set mass failed") 41 | } 42 | 43 | dm2 := &DeltaMass{ 44 | Mass{-10}, 45 | NewVector(0, 0), 46 | } 47 | 48 | if Push(v3, dm2) == nil { 49 | t.Fatalf("pushing negative delta mass did not fail") 50 | } 51 | if Push(v3, dm) != nil { 52 | t.Fatalf("pushing positive delta mass failed") 53 | } 54 | 55 | dm2.Freeze() 56 | 57 | if Push(v3, dm2) != nil { 58 | t.Fatalf("pushing frozen delta mass failed") 59 | } 60 | 61 | // Todo: test that pushing results in expected changes 62 | } 63 | -------------------------------------------------------------------------------- /render/bachload_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/oakmound/oak/v4/oakerr" 8 | ) 9 | 10 | func TestBlankBatchLoad_BadBaseFolder(t *testing.T) { 11 | err := BlankBatchLoad("notfound", 0) 12 | expected := oakerr.InvalidInput{} 13 | if !errors.As(err, &expected) { 14 | t.Fatalf("error was not expected invalid input: %v", err) 15 | } 16 | } 17 | 18 | func TestBatchLoad(t *testing.T) { 19 | if BatchLoad("testdata/assets/images") != nil { 20 | t.Fatalf("batch load failed") 21 | } 22 | sh, err := GetSheet("jeremy.png") 23 | if err != nil { 24 | t.Fatalf("get sheet failed: %v", err) 25 | } 26 | if len(sh.ToSprites()) != 8 { 27 | t.Fatalf("sheet did not contain 8 sprites") 28 | } 29 | _, err = LoadSprite("dir/dummy.jpg") 30 | if err == nil { 31 | t.Fatalf("load sprite should have failed") 32 | } 33 | sp, err := GetSprite("dummy.gif") 34 | if sp != nil { 35 | t.Fatalf("get sprite should be nil") 36 | } 37 | if err == nil { 38 | t.Fatalf("get sprite should have failed") 39 | } 40 | sp, err = GetSprite("jeremy.png") 41 | if sp == nil { 42 | t.Fatalf("get sprite failed") 43 | } 44 | if err != nil { 45 | t.Fatalf("get sprite failed: %v", err) 46 | } 47 | DefaultCache.ClearAll() 48 | } 49 | -------------------------------------------------------------------------------- /render/bezier_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/oakmound/oak/v4/shape" 8 | ) 9 | 10 | func TestSimpleBezierLine(t *testing.T) { 11 | bz, err := shape.BezierCurve(0, 0, 10, 10) 12 | if err != nil { 13 | t.Fatalf("failed to create bezier curve: %v", err) 14 | } 15 | sp := BezierLine(bz, color.RGBA{255, 255, 255, 255}) 16 | rgba := sp.GetRGBA() 17 | for i := 0; i < 10; i++ { 18 | if rgba.At(i, i) != (color.RGBA{255, 255, 255, 255}) { 19 | t.Fatalf("rgba not set at %v", i) 20 | } 21 | } 22 | 23 | bz, err = shape.BezierCurve(10, 10, 0, 0) 24 | if err != nil { 25 | t.Fatalf("failed to create bezier curve: %v", err) 26 | } 27 | sp = BezierLine(bz, color.RGBA{255, 255, 255, 255}) 28 | rgba = sp.GetRGBA() 29 | for i := 0; i < 10; i++ { 30 | if rgba.At(i, i) != (color.RGBA{255, 255, 255, 255}) { 31 | t.Fatalf("rgba not set at %v", i) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /render/cache_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "testing" 4 | 5 | func TestCache_Clear(t *testing.T) { 6 | err := BatchLoad("testdata/assets/images") 7 | if err != nil { 8 | t.Fatalf("batch load failed: %v", err) 9 | } 10 | file := "jeremy.png" 11 | _, err = GetSprite(file) 12 | if err != nil { 13 | t.Fatalf("get jeremy should have succeeded: %v", err) 14 | } 15 | DefaultCache.Clear(file) 16 | _, err = GetSprite(file) 17 | if err == nil { 18 | t.Fatal("get jeremy should have failed post-Clear") 19 | } 20 | file = "testdata/assets/fonts/luxisr.ttf" 21 | _, err = LoadFont(file) 22 | if err != nil { 23 | t.Fatalf("load luxisr should have succeeded: %v", err) 24 | } 25 | DefaultCache.Clear(file) 26 | _, err = GetFont(file) 27 | if err == nil { 28 | t.Fatal("get luxisr should have failed post-Clear") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /render/colorbox_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | "testing/quick" 8 | ) 9 | 10 | func TestColorBox(t *testing.T) { 11 | c := color.RGBA{255, 200, 255, 255} 12 | sp := NewColorBox(5, 5, c) 13 | rgba := sp.GetRGBA() 14 | for x := 0; x < 5; x++ { 15 | for y := 0; y < 5; y++ { 16 | if rgba.At(x, y) != c { 17 | t.Fatalf("rgba not set at %v,%v", x, y) 18 | } 19 | } 20 | } 21 | if rgba.At(6, 6) != (color.RGBA{0, 0, 0, 0}) { 22 | t.Fatalf("rgba exceeded w/h") 23 | } 24 | } 25 | 26 | func TestColorBoxR(t *testing.T) { 27 | if err := quick.Check(testColorBoxRProperties, nil); err != nil { 28 | t.Error(err) 29 | } 30 | } 31 | 32 | // w and h are int8 because int16 can make us check 65536*65536 = 4 billion pixels which can time out 33 | func testColorBoxRProperties(r, g, b, a uint8, w8, h8 int8) bool { 34 | c := color.RGBA{r, g, b, a} 35 | w := int(w8) 36 | h := int(h8) 37 | sp := NewColorBoxR(w, h, c) 38 | w2, h2 := sp.GetDims() 39 | if w2 != w { 40 | return false 41 | } 42 | if h2 != h { 43 | return false 44 | } 45 | img := image.NewRGBA(image.Rect(0, 0, w, h)) 46 | sp.Draw(img, 0, 0) 47 | for x := 0; x < w; x++ { 48 | for y := 0; y < h; y++ { 49 | cAt := img.RGBAAt(x, y) 50 | if c != cAt { 51 | return false 52 | } 53 | } 54 | } 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /render/colorer.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "image/color" 4 | 5 | // A Colorer takes some notion of linear progress and returns a color 6 | type Colorer func(float64) color.Color 7 | 8 | // IdentityColorer returns the same color it was given at initialization, 9 | // regardless of progress. 10 | func IdentityColorer(c color.Color) Colorer { 11 | return func(float64) color.Color { 12 | return c 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /render/colorer_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "math/rand" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestIdentityColorer(t *testing.T) { 11 | rand.Seed(time.Now().UnixNano()) 12 | c := IdentityColorer(color.RGBA{255, 100, 100, 255}) 13 | if c(rand.Float64()) != (color.RGBA{255, 100, 100, 255}) { 14 | t.Fatalf("identity colorer did not return set color") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /render/decoder.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "io" 6 | 7 | "github.com/oakmound/oak/v4/oakerr" 8 | ) 9 | 10 | // Decoder functions convert arbitrary readers to images. 11 | // The input of a decoder in oak's loader will generally 12 | // be an image file. 13 | type Decoder func(io.Reader) (image.Image, error) 14 | 15 | // CfgDecoder is an equivalent to Decoder that just exports 16 | // the color model and dimensions of the image. 17 | type CfgDecoder func(io.Reader) (image.Config, error) 18 | 19 | var ( 20 | fileDecoders = map[string]Decoder{} 21 | cfgDecoders = map[string]CfgDecoder{} 22 | ) 23 | 24 | // RegisterDecoder adds a decoder to the set of image decoders 25 | // for file loading. If the extension string is already set, 26 | // the existing decoder will not be overwritten. 27 | func RegisterDecoder(ext string, decoder Decoder) error { 28 | _, ok := fileDecoders[ext] 29 | if ok { 30 | return oakerr.ExistingElement{ 31 | InputName: "ext", 32 | InputType: "string", 33 | Overwritten: false, 34 | } 35 | } 36 | fileDecoders[ext] = decoder 37 | return nil 38 | } 39 | 40 | // RegisterCfgDecoder acts like RegisterDecoder for CfgDecoders 41 | func RegisterCfgDecoder(ext string, decoder CfgDecoder) error { 42 | _, ok := cfgDecoders[ext] 43 | if ok { 44 | return oakerr.ExistingElement{ 45 | InputName: "ext", 46 | InputType: "string", 47 | Overwritten: false, 48 | } 49 | } 50 | cfgDecoders[ext] = decoder 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /render/decoder_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // We are rather intentionally not testing that the decoders work, 8 | // partially because it's a lot of work and partially because decoding images is not 9 | // this package's job (it just calls decoders). It'd be a lot of pointless mocking. 10 | 11 | func TestRegisterDecoder(t *testing.T) { 12 | err := RegisterDecoder(".png", nil) 13 | if err == nil { 14 | t.Fatal("expected registering .png to fail") 15 | } 16 | err = RegisterDecoder(".new", nil) 17 | if err != nil { 18 | t.Fatalf("expected registering .new to succeed: %v", err) 19 | } 20 | } 21 | 22 | func TestRegisterCfgDecoder(t *testing.T) { 23 | err := RegisterCfgDecoder(".png", nil) 24 | if err == nil { 25 | t.Fatal("expected registering .png to fail") 26 | } 27 | err = RegisterCfgDecoder(".new", nil) 28 | if err != nil { 29 | t.Fatalf("expected registering .new to succeed: %v", err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /render/default_decoders.go: -------------------------------------------------------------------------------- 1 | //go:build !noimages 2 | // +build !noimages 3 | 4 | package render 5 | 6 | import ( 7 | "image/gif" 8 | "image/jpeg" 9 | "image/png" 10 | 11 | "golang.org/x/image/bmp" 12 | ) 13 | 14 | func init() { 15 | // Register standard image decoders. If provided with the build tag 'noimages', this is skipped. 16 | RegisterDecoder(".jpeg", jpeg.Decode) 17 | RegisterDecoder(".jpg", jpeg.Decode) 18 | RegisterDecoder(".gif", gif.Decode) 19 | RegisterDecoder(".png", png.Decode) 20 | RegisterDecoder(".bmp", bmp.Decode) 21 | RegisterCfgDecoder(".jpeg", jpeg.DecodeConfig) 22 | RegisterCfgDecoder(".jpg", jpeg.DecodeConfig) 23 | RegisterCfgDecoder(".gif", gif.DecodeConfig) 24 | RegisterCfgDecoder(".png", png.DecodeConfig) 25 | RegisterCfgDecoder(".bmp", bmp.DecodeConfig) 26 | } 27 | -------------------------------------------------------------------------------- /render/default_font.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | // embed is used here to embed our default font file. 5 | _ "embed" 6 | "fmt" 7 | ) 8 | 9 | // Oak ships with a free font to enable text display without needing to set up 10 | // a font on the user's machine to import. This is that font. It is embedded into 11 | // the Go code to ensure it is not stripped from the code by vendoring, for example. 12 | // The file is called luxisr.ttf. 13 | 14 | //go:embed luxisr.ttf 15 | var luxisrTTF []byte 16 | 17 | // Functions in this file operate on the default font, and are equivalent to 18 | // DefaultFont().Call. DefaultFont() does perform work to generate the default font, 19 | // so storing the result and calling these functions on the stored Font is 20 | // recommended in cases where performance is a concern. 21 | 22 | // NewStringerText creates a text element using the default font and a stringer. 23 | func NewStringerText(str fmt.Stringer, x, y float64) *Text { 24 | return DefaultFont().NewStringerText(str, x, y) 25 | } 26 | 27 | // NewIntText wraps the given int pointer in a stringer interface and creates 28 | // a text renderable that will diplay the underlying int value. 29 | func NewIntText(str *int, x, y float64) *Text { 30 | return DefaultFont().NewIntText(str, x, y) 31 | } 32 | 33 | // NewText is a helper to create a text element with the default font and a string. 34 | func NewText(str string, x, y float64) *Text { 35 | return DefaultFont().NewText(str, x, y) 36 | } 37 | 38 | // NewStrPtrText is a helper to take in a string pointer for NewText 39 | func NewStrPtrText(str *string, x, y float64) *Text { 40 | return DefaultFont().NewStrPtrText(str, x, y) 41 | } 42 | -------------------------------------------------------------------------------- /render/default_font_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLegacyFont(t *testing.T) { 8 | if NewStringerText(dummyStringer{}, 0, 0) == nil { 9 | t.Fatalf("NewStringerText failed") 10 | } 11 | if NewText("text", 0, 0) == nil { 12 | t.Fatalf("NewText failed") 13 | } 14 | if NewIntText(new(int), 0, 0) == nil { 15 | t.Fatalf("NewIntText failed") 16 | } 17 | if NewStrPtrText(new(string), 0, 0) == nil { 18 | t.Fatalf("NewStrPtrText failed") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /render/doc.go: -------------------------------------------------------------------------------- 1 | // Package render provides structures for organizing graphics to draw to a window and 2 | // essential graphical primitives. 3 | package render 4 | -------------------------------------------------------------------------------- /render/draw.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | ) 6 | 7 | var ( 8 | // emptyRenderable is a simple renderable that can be used 9 | // for pseudo-nil renderables that need to be something 10 | emptyRenderable = NewColorBox(1, 1, color.RGBA{0, 0, 0, 0}) 11 | ) 12 | 13 | // EmptyRenderable returns a minimal, 1-width and height pseudo-nil Renderable 14 | func EmptyRenderable() Modifiable { 15 | return emptyRenderable.Copy() 16 | } 17 | 18 | // DrawColor is equivalent to LoadSpriteAndDraw, 19 | // but with colorboxes. 20 | func DrawColor(c color.Color, x, y, w, h float64, layers ...int) (Renderable, error) { 21 | cb := NewColorBox(int(w), int(h), c) 22 | cb.ShiftX(x) 23 | cb.ShiftY(y) 24 | return Draw(cb, layers...) 25 | } 26 | 27 | // DrawPoint draws a color on the screen as a single-widthed 28 | // pixel (box) 29 | func DrawPoint(c color.Color, x1, y1 float64, layers ...int) (Renderable, error) { 30 | return DrawColor(c, x1, y1, 1, 1, layers...) 31 | } 32 | -------------------------------------------------------------------------------- /render/drawStack_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/oakmound/oak/v4/alg/intgeom" 10 | ) 11 | 12 | func TestDrawStack(t *testing.T) { 13 | GlobalDrawStack.PreDraw() 14 | if len(GlobalDrawStack.as) != 1 { 15 | t.Fatalf("global draw stack did not have one length initially") 16 | } 17 | SetDrawStack( 18 | NewDynamicHeap(), 19 | NewStaticHeap(), 20 | ) 21 | if len(GlobalDrawStack.as) != 2 { 22 | t.Fatalf("global draw stack did not have two length after reset") 23 | } 24 | GlobalDrawStack.Pop() 25 | GlobalDrawStack.PreDraw() 26 | if len(GlobalDrawStack.as) != 1 { 27 | t.Fatalf("global draw stack did not have one length after pop") 28 | } 29 | cp := GlobalDrawStack.Copy() 30 | if len(cp.toPush) != len(GlobalDrawStack.toPush) { 31 | t.Fatalf("copy failed to copy push length") 32 | } 33 | } 34 | 35 | func TestDrawStack_Draw(t *testing.T) { 36 | _, err := Draw(nil) 37 | if err == nil { 38 | t.Fatalf("draw(nil) should have failed") 39 | } 40 | cb := NewColorBox(10, 10, color.RGBA{0, 0, 255, 255}) 41 | Draw(cb) 42 | GlobalDrawStack.Clear() 43 | SetDrawStack( 44 | NewDynamicHeap(), 45 | NewStaticHeap(), 46 | ) 47 | Draw(cb) 48 | rgba := image.NewRGBA(image.Rect(0, 0, 10, 10)) 49 | GlobalDrawStack.PreDraw() 50 | GlobalDrawStack.DrawToScreen(rgba, &intgeom.Point2{0, 0}, 10, 10) 51 | if !reflect.DeepEqual(rgba, cb.GetRGBA()) { 52 | t.Fatalf("rgba mismatch") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /render/draw_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func ExampleDraw() { 9 | // We haven't modified the draw stack, so it contains a single draw heap. 10 | // Draw a Color Box 11 | Draw(NewColorBox(10, 10, color.RGBA{255, 255, 255, 255}), 3) 12 | // Draw a Gradient Box above that color box 13 | Draw(NewHorizontalGradientBox(5, 5, color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}), 4) 14 | } 15 | 16 | func TestDrawHelpers(t *testing.T) { 17 | r, err := DrawColor(color.RGBA{255, 255, 255, 255}, 0, 0, 10, 10, 0, 0) 18 | if err != nil { 19 | t.Fatalf("draw color should not have failed") 20 | } 21 | if r == nil { 22 | t.Fatalf("draw color should not give nil renderable") 23 | } 24 | GlobalDrawStack.Push(&CompositeR{}) 25 | GlobalDrawStack.PreDraw() 26 | 27 | _, err = DrawColor(color.RGBA{255, 255, 255, 255}, 0, 0, 10, 10, 3, 0) 28 | if err == nil { 29 | t.Fatalf("draw color to invalid layer should fail") 30 | } 31 | 32 | _, err = DrawPoint(color.RGBA{100, 100, 100, 255}, 0, 0, 0) 33 | if err != nil { 34 | t.Fatalf("draw color should not have failed") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /render/fps_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func TestDrawFPS(t *testing.T) { 9 | dfps := NewDrawFPS(0, nil, 0, 0) 10 | dfps.Draw(image.NewRGBA(image.Rect(0, 0, 100, 100)), 10, 10) 11 | if dfps.fps == 0 { 12 | t.Fatalf("fps not set by draw") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /render/gradients.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | ) 6 | 7 | //GradientColorAt returns a new color via a gradient between two colors and the progress between them 8 | func GradientColorAt(c1, c2 color.Color, progress float64) color.RGBA64 { 9 | r, g, b, a := c1.RGBA() 10 | r2, g2, b2, a2 := c2.RGBA() 11 | return color.RGBA64{ 12 | uint16OnScale(r, r2, progress), 13 | uint16OnScale(g, g2, progress), 14 | uint16OnScale(b, b2, progress), 15 | uint16OnScale(a, a2, progress), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /render/gradients_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestGradient(t *testing.T) { 9 | a := color.RGBA{0, 0, 0, 0} 10 | b := color.RGBA{255, 255, 255, 255} 11 | for i := uint16(0); i < 255; i++ { 12 | progress := float64(i) / 255.0 13 | gc := GradientColorAt(a, b, progress) 14 | v := (i * 257) 15 | diff := v - gc.R 16 | if !(diff < 2) { 17 | t.Fatalf("gradient did not fall under expected precision") 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /render/interfaceFeatures.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "github.com/oakmound/oak/v4/event" 4 | 5 | // NonStatic types are not always static. If something is not NonStatic, 6 | // it is equivalent to having IsStatic always return true. 7 | type NonStatic interface { 8 | IsStatic() bool 9 | } 10 | 11 | // Triggerable types can have an ID set so when their animations finish, 12 | // they trigger AnimationEnd on that ID. 13 | type Triggerable interface { 14 | SetTriggerID(event.CallerID) 15 | } 16 | 17 | type updates interface { 18 | update() 19 | } 20 | -------------------------------------------------------------------------------- /render/interruptable.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | // NonInterruptable types are not always interruptable. If something is not 4 | // NonInterruptable, it is equivalent to having IsInterruptable always return 5 | // true. 6 | // 7 | // The intended use of the Interruptable datatypes is entirely external-- 8 | // oak does not use them internally. The use case is for an entity that has 9 | // a set of potential animations, and attempts to switch from one animation 10 | // to another. The Interuptable boolean should represent whether that 11 | // animation should be able to be switched out of before it ends. 12 | // 13 | // Because this use case is minor, this is a candidate for removal from render 14 | // and moving into an auxiliary package. 15 | // 16 | // Unless otherwise noted, all NonInterruptable types are interruptable when 17 | // they are initialized and need to be switched (if the type supports it) to 18 | // be non interruptable. 19 | type NonInterruptable interface { 20 | IsInterruptable() bool 21 | } 22 | 23 | // InterruptBool is a composable struct for NonInterruptable support 24 | type InterruptBool struct { 25 | Interruptable bool 26 | } 27 | 28 | // IsInterruptable returns whether this can be interrupted. 29 | func (ib InterruptBool) IsInterruptable() bool { 30 | return ib.Interruptable 31 | } 32 | -------------------------------------------------------------------------------- /render/layered_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLayeredNils(t *testing.T) { 8 | var ld *Layer 9 | if ld.GetLayer() != Undraw { 10 | t.Fatalf("nil layer should be undrawn") 11 | } 12 | var ldp *LayeredPoint 13 | if ldp.GetLayer() != Undraw { 14 | t.Fatalf("nil layered point should be undrawn") 15 | } 16 | w, h := ldp.GetDims() 17 | if w != 1 || h != 1 { 18 | t.Fatalf("GetDims faailed") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /render/loadsprite_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func Test_loadSpriteNoCache_maxFileSize(t *testing.T) { 9 | rgba, err := loadSpriteNoCache("testdata/assets/images/16x16/jeremy.png", 1) 10 | if err != nil { 11 | t.Fatalf("failed to load jeremy: %v", err) 12 | } 13 | if rgba == nil { 14 | t.Fatalf("failed to load jeremy rgba") 15 | } 16 | for x := rgba.Rect.Min.X; x < rgba.Rect.Max.X; x++ { 17 | for y := rgba.Rect.Min.Y; y < rgba.Rect.Max.Y; y++ { 18 | c := rgba.RGBAAt(x, y) 19 | if c != (color.RGBA{0, 0, 0, 0}) { 20 | t.Fatal("image was not blank") 21 | } 22 | } 23 | } 24 | } 25 | 26 | func Test_loadSpriteNoCache_maxFileSize_badImage(t *testing.T) { 27 | _, err := loadSpriteNoCache("testdata/assets/images/16x16/bad.png", 1) 28 | if err == nil { 29 | t.Fatalf("loading bad file should have errored") 30 | } 31 | } 32 | 33 | func Test_loadSpriteNoCache_badFileExtension(t *testing.T) { 34 | _, err := loadSpriteNoCache("testdata/assets/images/devfile.pdn", 0) 35 | if err == nil { 36 | t.Fatalf("loading pdn file should have errored") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /render/logicfps.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/oakmound/oak/v4/event" 7 | "github.com/oakmound/oak/v4/timing" 8 | ) 9 | 10 | // LogicFPS is a Stackable that will draw the logical fps onto the screen when a part 11 | // of the draw stack. 12 | type LogicFPS struct { 13 | event.CallerID 14 | *Text 15 | fps int 16 | lastTime time.Time 17 | Smoothing float64 18 | } 19 | 20 | func (lf LogicFPS) CID() event.CallerID { 21 | return lf.CallerID 22 | } 23 | 24 | // NewLogicFPS returns a LogicFPS, which will render a counter of how fast it receives event.Enter events. 25 | // If font is not provided, DefaultFont is used. If smoothing is 0, a reasonable default is used. 26 | func NewLogicFPS(smoothing float64, font *Font, x, y float64) *LogicFPS { 27 | if smoothing == 0.0 { 28 | smoothing = defaultFpsSmoothing 29 | } 30 | if font == nil { 31 | font = DefaultFont().Copy() 32 | } 33 | lf := &LogicFPS{ 34 | Smoothing: smoothing, 35 | lastTime: time.Now(), 36 | } 37 | lf.Text = font.NewIntText(&lf.fps, x, y) 38 | lf.CallerID = event.DefaultCallerMap.Register(lf) 39 | // TODO: not default bus 40 | event.Bind(event.DefaultBus, event.Enter, lf, logicFPSBind) 41 | 42 | return lf 43 | } 44 | 45 | func logicFPSBind(lf *LogicFPS, _ event.EnterPayload) event.Response { 46 | t := time.Now() 47 | lf.fps = int((timing.FPS(lf.lastTime, t) * lf.Smoothing) + (float64(lf.fps) * (1 - lf.Smoothing))) 48 | lf.lastTime = t 49 | return 0 50 | } 51 | -------------------------------------------------------------------------------- /render/logicfps_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/oakmound/oak/v4/event" 8 | ) 9 | 10 | func TestLogicFPS(t *testing.T) { 11 | lfps := NewLogicFPS(0, nil, 0, 0) 12 | lfps.Draw(image.NewRGBA(image.Rect(0, 0, 100, 100)), 10, 10) 13 | logicFPSBind(lfps, event.EnterPayload{}) 14 | if lfps.fps == 0 { 15 | t.Fatalf("fps not set by binding") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /render/luxisr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/luxisr.ttf -------------------------------------------------------------------------------- /render/mod/doc.go: -------------------------------------------------------------------------------- 1 | // Package mod stores modification functions for images. 2 | package mod 3 | -------------------------------------------------------------------------------- /render/modifiable.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/oakmound/oak/v4/render/mod" 7 | ) 8 | 9 | // A Modifiable is a Renderable that has functions to change its 10 | // underlying image. 11 | type Modifiable interface { 12 | Renderable 13 | GetRGBA() *image.RGBA 14 | Modify(...mod.Mod) Modifiable 15 | Filter(...mod.Filter) 16 | Copy() Modifiable 17 | } 18 | -------------------------------------------------------------------------------- /render/noopStackable.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/draw" 5 | 6 | "github.com/oakmound/oak/v4/alg/intgeom" 7 | ) 8 | 9 | // NoopStackable is a Stackable element where all methods are no-ops. 10 | // Use for tests to disable rendering. 11 | type NoopStackable struct{} 12 | 13 | // PreDraw on a NoopStackable does nothing. 14 | func (ns NoopStackable) PreDraw() {} 15 | 16 | // Add on a NoopStackable does nothing. The input Renderable is still returned. 17 | func (ns NoopStackable) Add(r Renderable, _ ...int) Renderable { 18 | return r 19 | } 20 | 21 | // Replace on a NoopStackable does nothing. 22 | func (ns NoopStackable) Replace(Renderable, Renderable, int) {} 23 | 24 | // Copy on a NoopStackable returns itself. 25 | func (ns NoopStackable) Copy() Stackable { 26 | return ns 27 | } 28 | 29 | // DrawToScreen on a NoopStackable does nothing. 30 | func (ns NoopStackable) DrawToScreen(draw.Image, *intgeom.Point2, int, int) {} 31 | 32 | // Clear on a NoopStackable does nothing. 33 | func (ns NoopStackable) Clear() {} 34 | -------------------------------------------------------------------------------- /render/noopStackable_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/alg/intgeom" 7 | ) 8 | 9 | func TestNoopStackable(t *testing.T) { 10 | noop := NoopStackable{} 11 | // these calls are noops 12 | noop.PreDraw() 13 | noop.Replace(nil, nil, -142) 14 | noop.DrawToScreen(nil, &intgeom.Point2{}, -124, 23) 15 | r := noop.Add(nil, 01, 124, 04, 2) 16 | if r != nil { 17 | t.Fatalf("expected nil renderable from Add, got %v", r) 18 | } 19 | noop2 := noop.Copy() 20 | if noop2 != noop { 21 | t.Fatalf("expected equal noop stackables") 22 | } 23 | noop.Clear() 24 | } 25 | -------------------------------------------------------------------------------- /render/particle/allocator_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/event" 7 | ) 8 | 9 | func TestAllocate(t *testing.T) { 10 | a := NewAllocator() 11 | go a.Run() 12 | for i := 0; i < 100; i++ { 13 | if a.Allocate(event.CallerID(i)) != i { 14 | t.Fatalf("expected allocation of id %d to match id", i) 15 | } 16 | } 17 | } 18 | 19 | func TestDeallocate(t *testing.T) { 20 | a := NewAllocator() 21 | go a.Run() 22 | 23 | a.Allocate(0) 24 | a.Deallocate(0) 25 | 26 | if a.Allocate(0) != 0 { 27 | t.Fatalf("expected allocation of id %d to match id", 0) 28 | } 29 | } 30 | 31 | func TestAllocatorLookup(t *testing.T) { 32 | a := NewAllocator() 33 | go a.Run() 34 | 35 | src := NewDefaultSource(NewColorGenerator(), 0) 36 | cid := src.CID() 37 | pidBlock := a.Allocate(cid) 38 | src2 := a.LookupSource(pidBlock * blockSize) 39 | if src != src2 { 40 | t.Fatalf("Lookup on first block did not obtain allocated source") 41 | } 42 | 43 | src3 := a.Lookup((pidBlock * blockSize) + 1) 44 | if src3 != nil { 45 | t.Fatalf("Lookup on second block did not return nil") 46 | } 47 | a.Deallocate(2) 48 | a.Deallocate(1) 49 | a.Deallocate(0) 50 | } 51 | -------------------------------------------------------------------------------- /render/particle/collisionParticle.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image/draw" 5 | 6 | "github.com/oakmound/oak/v4/collision" 7 | ) 8 | 9 | // A CollisionParticle is a wrapper around other particles that also 10 | // has a collision space and can functionally react with the environment 11 | // on collision 12 | type CollisionParticle struct { 13 | Particle 14 | s *collision.ReactiveSpace 15 | } 16 | 17 | // Draw redirects to DrawOffsetGen 18 | func (cp *CollisionParticle) Draw(buff draw.Image, xOff, yOff float64) { 19 | cp.DrawOffsetGen(cp.Particle.GetBaseParticle().Src.Generator, buff, xOff, yOff) 20 | } 21 | 22 | // DrawOffsetGen draws a particle with it's generator's variables 23 | func (cp *CollisionParticle) DrawOffsetGen(generator Generator, buff draw.Image, xOff, yOff float64) { 24 | gen := generator.(*CollisionGenerator) 25 | cp.Particle.DrawOffsetGen(gen.Generator, buff, xOff, yOff) 26 | } 27 | 28 | // Cycle updates the collision particles variables once per rotation 29 | func (cp *CollisionParticle) Cycle(generator Generator) { 30 | gen := generator.(*CollisionGenerator) 31 | pos := cp.Particle.GetPos() 32 | cp.s.Space.Location = collision.NewRect(pos.X(), pos.Y(), cp.s.GetW(), cp.s.GetH()) 33 | 34 | hitFlag := <-cp.s.CallOnHits() 35 | if gen.Fragile && hitFlag { 36 | cp.Particle.GetBaseParticle().Life = 0 37 | } 38 | } 39 | 40 | // GetDims returns the dimensions of the space of the particle 41 | func (cp *CollisionParticle) GetDims() (int, int) { 42 | return int(cp.s.GetW()), int(cp.s.GetH()) 43 | } 44 | -------------------------------------------------------------------------------- /render/particle/collision_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/oakmound/oak/v4/collision" 8 | ) 9 | 10 | func TestCollisionParticle(t *testing.T) { 11 | hm := map[collision.Label]collision.OnHit{ 12 | 1: func(_, _ *collision.Space) {}, 13 | } 14 | g := NewCollisionGenerator(NewColorGenerator(), HitMap(hm), Fragile(true)).(*CollisionGenerator) 15 | src := g.Generate(0) 16 | src.addParticles() 17 | cp := src.particles[0].(*CollisionParticle) 18 | w, h := cp.GetDims() 19 | if w != 1 { 20 | t.Fatalf("expected 1 width, got %v", w) 21 | } 22 | if h != 1 { 23 | t.Fatalf("expected 1 height, got %v", h) 24 | } 25 | cp.Draw(image.NewRGBA(image.Rect(0, 0, 20, 20)), 0, 0) 26 | cp.Cycle(g) 27 | collision.Add(collision.NewLabeledSpace(-20, -20, 40, 40, 1)) 28 | cp.Cycle(g) 29 | 30 | _, _, ok := g.GetParticleSize() 31 | if !ok { 32 | t.Fatalf("get particle size not particle-specified") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /render/particle/color.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image/color" 5 | ) 6 | 7 | // A Colorable can have colors set on it. 8 | type Colorable interface { 9 | SetStartColor(color.Color, color.Color) 10 | SetEndColor(color.Color, color.Color) 11 | } 12 | 13 | // Color sets colors on a Colorable 14 | func Color(start, startRand, end, endRand color.Color) func(Generator) { 15 | return func(g Generator) { 16 | if c, ok := g.(Colorable); ok { 17 | c.SetStartColor(start, startRand) 18 | c.SetEndColor(end, endRand) 19 | } 20 | } 21 | } 22 | 23 | // A Colorable2 can have more colors set on it 24 | type Colorable2 interface { 25 | SetStartColor2(color.Color, color.Color) 26 | SetEndColor2(color.Color, color.Color) 27 | } 28 | 29 | // Color2 sets more colors on a Colorable2 30 | func Color2(start, startRand, end, endRand color.Color) func(Generator) { 31 | return func(g Generator) { 32 | if c, ok := g.(Colorable2); ok { 33 | c.SetStartColor2(start, startRand) 34 | c.SetEndColor2(end, endRand) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /render/particle/color_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/oakmound/oak/v4/alg/span" 9 | "github.com/oakmound/oak/v4/render" 10 | "github.com/oakmound/oak/v4/shape" 11 | ) 12 | 13 | func TestColorParticle(t *testing.T) { 14 | g := NewColorGenerator( 15 | Rotation(span.NewConstant(1.0)), 16 | Color(color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}, color.RGBA{255, 0, 0, 255}), 17 | Size(span.NewConstant(5)), 18 | EndSize(span.NewConstant(10)), 19 | Shape(shape.Heart), 20 | ) 21 | src := g.Generate(0) 22 | src.addParticles() 23 | 24 | p := src.particles[0].(*ColorParticle) 25 | 26 | p.Draw(image.NewRGBA(image.Rect(0, 0, 20, 20)), 0, 0) 27 | if p.GetLayer() != 0 { 28 | t.Fatalf("expected 0 layer, got %v", p.GetLayer()) 29 | } 30 | 31 | p.Life = -1 32 | sz, _ := p.GetDims() 33 | if sz != int(p.endSize) { 34 | t.Fatalf("expected size %v at end of particle's life, got %v", p.endSize, sz) 35 | } 36 | p.Draw(image.NewRGBA(image.Rect(0, 0, 20, 20)), 0, 0) 37 | 38 | var cp2 *ColorParticle 39 | if cp2.GetLayer() != render.Undraw { 40 | t.Fatalf("uninitialized particle was not set to the undraw layer") 41 | } 42 | 43 | _, _, ok := g.GetParticleSize() 44 | if !ok { 45 | t.Fatalf("get particle size not particle-specified") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /render/particle/doc.go: -------------------------------------------------------------------------------- 1 | // Package particle provides options for generating renderable particle sources. 2 | package particle 3 | -------------------------------------------------------------------------------- /render/particle/generator_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBaseGenerator(t *testing.T) { 8 | bg := new(BaseGenerator) 9 | bg.setDefaults() 10 | bg.ShiftX(10) 11 | bg.ShiftY(10) 12 | if bg.X() != 10 { 13 | t.Fatalf("expected 10 x, got %v", bg.X()) 14 | } 15 | if bg.Y() != 10 { 16 | t.Fatalf("expected 10 y, got %v", bg.Y()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /render/particle/gradientParticle.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image/color" 5 | "image/draw" 6 | 7 | "github.com/oakmound/oak/v4/render" 8 | ) 9 | 10 | // A GradientParticle has a gradient from one color to another 11 | type GradientParticle struct { 12 | ColorParticle 13 | startColor2 color.Color 14 | endColor2 color.Color 15 | } 16 | 17 | // Draw redirects to DrawOffsetGen 18 | func (gp *GradientParticle) Draw(buff draw.Image, xOff, yOff float64) { 19 | gp.DrawOffsetGen(gp.GetBaseParticle().Src.Generator, buff, xOff, yOff) 20 | } 21 | 22 | // DrawOffsetGen draws a particle with it's generator's variables 23 | func (gp *GradientParticle) DrawOffsetGen(generator Generator, buff draw.Image, xOff, yOff float64) { 24 | 25 | gen := generator.(*GradientGenerator) 26 | progress := gp.Life / gp.totalLife 27 | c1 := render.GradientColorAt(gp.startColor, gp.endColor, progress) 28 | c2 := render.GradientColorAt(gp.startColor2, gp.endColor2, progress) 29 | 30 | size := int(((1 - progress) * gp.size) + (progress * gp.endSize)) 31 | 32 | halfSize := float64(size) / 2 33 | 34 | xOffi := int(xOff - halfSize) 35 | yOffi := int(yOff - halfSize) 36 | 37 | for i := 0; i < size; i++ { 38 | for j := 0; j < size; j++ { 39 | if gen.Shape.In(i, j, size) { 40 | progress := gen.ProgressFunction(i, j, size, size) 41 | c := render.GradientColorAt(c1, c2, progress) 42 | buff.Set(xOffi+i, yOffi+j, c) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /render/particle/math.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | "math/rand" 7 | 8 | "github.com/oakmound/oak/v4/alg" 9 | ) 10 | 11 | // floatFromSpread returns a random value between 12 | // 0 and a given float64 f 13 | func floatFromSpread(f float64) float64 { 14 | return (f * 2 * rand.Float64()) - f 15 | } 16 | 17 | // randColor returns a random color from two arguments: 18 | // a base color and a color representing the maximum 19 | // potential offset for each of R,G,B, and A. 20 | func randColor(c, ra color.Color) color.Color { 21 | r, g, b, a := c.RGBA() 22 | r2, g2, b2, a2 := ra.RGBA() 23 | return color.RGBA64{ 24 | uint16Spread(r, r2), 25 | uint16Spread(g, g2), 26 | uint16Spread(b, b2), 27 | uint16Spread(a, a2), 28 | } 29 | } 30 | 31 | // uint16Spread returns a random uint16 between 32 | // n-r/2 and n+r/2, not higher than 2^16-1 33 | func uint16Spread(n, r uint32) uint16 { 34 | return uint16(math.Min(float64(int(n)+alg.RoundF64(floatFromSpread(float64(r)))), 65535.0)) 35 | } 36 | 37 | // uint16OnScale returns a uint16, progress % between n and endN. 38 | // At 0 progress, endN will be returned. At 1 progress, n will be returned. 39 | func uint16OnScale(n, endN uint32, progress float64) uint16 { 40 | return uint16((float64(n) - float64(n)*(1.0-progress) + float64(endN)*(1.0-progress))) 41 | } 42 | -------------------------------------------------------------------------------- /render/particle/options_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestInfiniteLifeSpan(t *testing.T) { 9 | g := &GradientGenerator{} 10 | InfiniteLifeSpan()(g) 11 | if g.LifeSpan.Poll() != math.MaxFloat64 { 12 | t.Fatalf("Infinite Life Span did not poll math.MaxFloat64") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /render/particle/particle.go: -------------------------------------------------------------------------------- 1 | // Package particle provides options for generating renderable 2 | // particle sources. 3 | package particle 4 | 5 | import ( 6 | "image/draw" 7 | 8 | "github.com/oakmound/oak/v4/physics" 9 | "github.com/oakmound/oak/v4/render" 10 | ) 11 | 12 | // A Particle is a renderable that is spawned by a generator, usually very fast, 13 | // usually very small, for visual effects 14 | type Particle interface { 15 | render.Renderable 16 | GetBaseParticle() *baseParticle 17 | GetPos() physics.Vector 18 | DrawOffsetGen(gen Generator, buff draw.Image, xOff, yOff float64) 19 | Cycle(gen Generator) 20 | setPID(int) 21 | } 22 | 23 | type baseParticle struct { 24 | render.LayeredPoint 25 | Src *Source 26 | Vel physics.Vector 27 | Life float64 28 | totalLife float64 29 | pID int 30 | } 31 | 32 | func (bp *baseParticle) GetLayer() int { 33 | if bp == nil { 34 | return render.Undraw 35 | } 36 | return bp.LayeredPoint.GetLayer() 37 | } 38 | 39 | func (bp *baseParticle) GetBaseParticle() *baseParticle { 40 | return bp 41 | } 42 | 43 | func (bp *baseParticle) GetPos() physics.Vector { 44 | return bp.Vector 45 | } 46 | 47 | func (bp *baseParticle) GetDims() (int, int) { 48 | return 0, 0 49 | } 50 | 51 | func (bp *baseParticle) Cycle(gen Generator) {} 52 | 53 | func (bp *baseParticle) setPID(pid int) { 54 | bp.pID = pid 55 | } 56 | -------------------------------------------------------------------------------- /render/particle/particle_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/render" 7 | ) 8 | 9 | func TestParticle(t *testing.T) { 10 | var bp *baseParticle 11 | w, h := bp.GetDims() 12 | if w != 0 { 13 | t.Fatalf("expected 0 x, got %v", w) 14 | } 15 | if h != 0 { 16 | t.Fatalf("expected 0 y, got %v", h) 17 | } 18 | 19 | if bp.GetLayer() != render.Undraw { 20 | t.Fatalf("uninitialized particle was not set to the undraw layer") 21 | } 22 | 23 | bp = new(baseParticle) 24 | bp.setPID(100) 25 | if bp.pID != 100 { 26 | t.Fatalf("setPID failed, expected 100 got %v", bp.pID) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /render/particle/shape.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import "github.com/oakmound/oak/v4/shape" 4 | 5 | // Shapeable generators can have the Shape option called on them 6 | type Shapeable interface { 7 | SetShape(shape.Shape) 8 | } 9 | 10 | // Shape is an option to set a generator's shape 11 | func Shape(sf shape.Shape) func(Generator) { 12 | return func(g Generator) { 13 | g.(Shapeable).SetShape(sf) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /render/particle/spriteParticle.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image/draw" 5 | 6 | "github.com/oakmound/oak/v4/render" 7 | "github.com/oakmound/oak/v4/render/mod" 8 | ) 9 | 10 | // A SpriteParticle is a particle that has an amount of sprite rotation 11 | type SpriteParticle struct { 12 | *baseParticle 13 | rotation float32 14 | } 15 | 16 | // Draw redirects to DrawOffsetGen 17 | func (sp *SpriteParticle) Draw(buff draw.Image, xOff, yOff float64) { 18 | sp.DrawOffsetGen(sp.GetBaseParticle().Src.Generator, buff, xOff, yOff) 19 | } 20 | 21 | // DrawOffsetGen draws a particle with it's generator's variables 22 | func (sp *SpriteParticle) DrawOffsetGen(generator Generator, buff draw.Image, xOff, yOff float64) { 23 | sp.rotation += sp.rotation 24 | gen := generator.(*SpriteGenerator) 25 | rgba := gen.Base.Copy().Modify(mod.Rotate(sp.rotation)).GetRGBA() 26 | render.DrawImage(buff, rgba, int(sp.X()+xOff), int(sp.Y()+yOff)) 27 | } 28 | -------------------------------------------------------------------------------- /render/particle/sprite_test.go: -------------------------------------------------------------------------------- 1 | package particle 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/oakmound/oak/v4/alg/span" 9 | "github.com/oakmound/oak/v4/render" 10 | ) 11 | 12 | func TestSpriteParticle(t *testing.T) { 13 | s := render.NewColorBox(10, 10, color.RGBA{255, 0, 0, 255}) 14 | g := NewSpriteGenerator( 15 | Sprite(s), 16 | Rotation(span.NewConstant(1.0)), 17 | SpriteRotation(span.NewConstant(1.0)), 18 | ) 19 | src := g.Generate(0) 20 | src.addParticles() 21 | 22 | p := src.particles[0].(*SpriteParticle) 23 | 24 | p.Draw(image.NewRGBA(image.Rect(0, 0, 20, 20)), 0, 0) 25 | 26 | w, h, ok := g.GetParticleSize() 27 | if w != 10 { 28 | t.Fatalf("expected 10 x, got %v", w) 29 | } 30 | if h != 10 { 31 | t.Fatalf("expected 10 y, got %v", h) 32 | } 33 | if ok { 34 | t.Fatalf("sprite particle generator should not have particle-specific sizes") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /render/pausing.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | // CanPause types have pause functions to start and stop animation 4 | type CanPause interface { 5 | Pause() 6 | Unpause() 7 | } 8 | 9 | type pauseBool struct { 10 | playing bool 11 | } 12 | 13 | func (p *pauseBool) Pause() { 14 | p.playing = false 15 | } 16 | 17 | func (p *pauseBool) Unpause() { 18 | p.playing = true 19 | } 20 | -------------------------------------------------------------------------------- /render/polygon_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/oakmound/oak/v4/alg/floatgeom" 8 | ) 9 | 10 | func TestContains(t *testing.T) { 11 | p := NewPointsPolygon( 12 | floatgeom.Point2{10, 10}, 13 | floatgeom.Point2{20, 10}, 14 | floatgeom.Point2{10, 20}, 15 | ) 16 | p.Fill(color.RGBA{255, 0, 0, 255}) 17 | if p.GetRGBA().At(1, 1) != (color.RGBA{255, 0, 0, 255}) { 18 | t.Fatalf("Fill did not hit 1,1") 19 | } 20 | p.FillInverse(color.RGBA{0, 255, 0, 255}) 21 | if p.GetRGBA().At(1, 1) != (color.RGBA{0, 255, 0, 255}) { 22 | t.Fatalf("FillInverse did not hit 1,1") 23 | } 24 | } 25 | 26 | func TestPolygonFns(t *testing.T) { 27 | p := NewPointsPolygon( 28 | floatgeom.Point2{0, 0}, 29 | floatgeom.Point2{0, 10}, 30 | floatgeom.Point2{10, 10}, 31 | floatgeom.Point2{10, 0}, 32 | ) 33 | cmp := p.GetOutline(color.RGBA{255, 0, 0, 255}) 34 | if len(cmp.rs) != 4 { 35 | t.Fatalf("composite did not contain four lines") 36 | } 37 | cmp = p.GetThickOutline(color.RGBA{255, 0, 0, 255}, 1) 38 | if len(cmp.rs) != 4 { 39 | t.Fatalf("composite did not contain four lines") 40 | } 41 | cmp = p.GetGradientOutline(color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}, 1) 42 | if len(cmp.rs) != 4 { 43 | t.Fatalf("composite did not contain four lines") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /render/renderable.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/draw" 5 | 6 | "github.com/oakmound/oak/v4/physics" 7 | ) 8 | 9 | // A Renderable is anything which can 10 | // be drawn at a given draw layer, undrawn, 11 | // and set in a particular position. 12 | // 13 | // Basic Implementing struct: Sprite 14 | type Renderable interface { 15 | Draw(buff draw.Image, xOff, yOff float64) 16 | GetDims() (width int, height int) 17 | 18 | Positional 19 | Layered 20 | physics.Attachable 21 | } 22 | 23 | // Positional types have 2D positions on a screen and can be manipulated 24 | // to be in a certain position on that screen. 25 | // 26 | // Basic Implementing struct: physics.Vector 27 | type Positional interface { 28 | X() float64 29 | Y() float64 30 | ShiftX(x float64) 31 | ShiftY(y float64) 32 | SetPos(x, y float64) 33 | } 34 | -------------------------------------------------------------------------------- /render/sheet.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/oakmound/oak/v4/oakerr" 7 | ) 8 | 9 | // Sheet is a 2D array of image rgbas 10 | type Sheet [][]*image.RGBA 11 | 12 | // SubSprite gets a sprite from a sheet at the given location 13 | func (sh *Sheet) SubSprite(x, y int) *Sprite { 14 | return NewSprite(0, 0, (*sh)[x][y]) 15 | } 16 | 17 | // ToSprites returns this sheet as a 2D array of Sprites 18 | func (sh *Sheet) ToSprites() [][]*Sprite { 19 | sprites := make([][]*Sprite, len(*sh)) 20 | for x, row := range *sh { 21 | sprites[x] = make([]*Sprite, len(row)) 22 | for y := range row { 23 | sprites[x][y] = sh.SubSprite(x, y) 24 | } 25 | } 26 | return sprites 27 | } 28 | 29 | // NewSheetSequence creates a Sequence from a sheet and a list of x,y frame coordinates. 30 | // A sequence will be created by getting the sheet's [i][i+1]th elements incrementally 31 | // from the input frames. If the number of input frames is uneven, an error is returned. 32 | func NewSheetSequence(sheet *Sheet, fps float64, frames ...int) (*Sequence, error) { 33 | if len(frames)%2 != 0 { 34 | return nil, oakerr.IndivisibleInput{ 35 | InputName: "frames", 36 | MustDivideBy: 2, 37 | } 38 | } 39 | 40 | sh := *sheet 41 | 42 | mods := make([]Modifiable, len(frames)/2) 43 | for i := 0; i < len(frames); i += 2 { 44 | if len(sh) <= frames[i] || len(sh[frames[i]]) <= frames[i+1] { 45 | return nil, oakerr.InvalidInput{InputName: "Frame requested does not exist "} 46 | } 47 | mods[i/2] = NewSprite(0, 0, sh[frames[i]][frames[i+1]]) 48 | } 49 | return NewSequence(fps, mods...), nil 50 | } 51 | -------------------------------------------------------------------------------- /render/sheet_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "embed" 5 | "os" 6 | "testing" 7 | 8 | "github.com/oakmound/oak/v4/alg/intgeom" 9 | "github.com/oakmound/oak/v4/fileutil" 10 | ) 11 | 12 | //go:embed testdata/assets/* 13 | var testfs embed.FS 14 | 15 | func TestMain(m *testing.M) { 16 | fileutil.FS = testfs 17 | os.Exit(m.Run()) 18 | } 19 | 20 | func TestSheetSequence(t *testing.T) { 21 | _, err := NewSheetSequence(nil, 10, 0) 22 | if err == nil { 23 | t.Fatalf("new sheet sequence with no sheet should fail") 24 | } 25 | 26 | sheet, err := LoadSheet("testdata/assets/images/16x16/jeremy.png", intgeom.Point2{16, 16}) 27 | if err != nil { 28 | t.Fatalf("loading jeremy sheet should not fail") 29 | } 30 | _, err = NewSheetSequence(sheet, 10, 0, 1, 0, 2) 31 | if err != nil { 32 | t.Fatalf("creating jeremy sheet sequence should not fail") 33 | } 34 | 35 | _, err = NewSheetSequence(sheet, 10, 100, 1, 0, 2) 36 | if err == nil { 37 | t.Fatalf("creating jeremy sheet sequence with invalid frames should fail") 38 | } 39 | 40 | _, err = NewSheetSequence(sheet, 10, 1, 100) 41 | if err == nil { 42 | t.Fatalf("creating jeremy sheet sequence with invalid frames should fail") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /render/shiny.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | // DrawImage performs a draw operation at -x, -y, because 9 | // shiny/screen represents quadrant 4 as negative in both axes. 10 | // draw.Over will merge two pixels at a given position based on their 11 | // alpha channel. 12 | func DrawImage(buff draw.Image, img image.Image, x, y int) { 13 | draw.Draw(buff, buff.Bounds(), 14 | img, image.Point{-x, -y}, draw.Over) 15 | } 16 | 17 | // OverwriteImage is equivalent to ShinyDraw, but uses draw.Src 18 | // draw.Src will overwrite pixels beneath the given image regardless of 19 | // the new image's alpha. 20 | func OverwriteImage(buff draw.Image, img image.Image, x, y int) { 21 | draw.Draw(buff, buff.Bounds(), 22 | img, image.Point{-x, -y}, draw.Src) 23 | } 24 | -------------------------------------------------------------------------------- /render/shiny_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | ) 8 | 9 | func TestShinyDrawFns(t *testing.T) { 10 | world := image.NewRGBA(image.Rect(0, 0, 20, 20)) 11 | rgba := image.NewRGBA(image.Rect(0, 0, 20, 20)) 12 | 13 | world.SetRGBA(10, 10, color.RGBA{255, 0, 0, 255}) 14 | 15 | DrawImage(world, rgba, 0, 0) 16 | if world.At(10, 10) != (color.RGBA{255, 0, 0, 255}) { 17 | t.Fatalf("draw image overwrote rgba") 18 | } 19 | OverwriteImage(world, rgba, 0, 0) 20 | if world.At(10, 10) != (color.RGBA{0, 0, 0, 0}) { 21 | t.Fatalf("overwrite image did not overwrite rgba") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /render/testdata/assets/fonts/luxisr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/fonts/luxisr.ttf -------------------------------------------------------------------------------- /render/testdata/assets/fonts/seguiemj.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/fonts/seguiemj.ttf -------------------------------------------------------------------------------- /render/testdata/assets/images/16x16/bad.png: -------------------------------------------------------------------------------- 1 | baddata -------------------------------------------------------------------------------- /render/testdata/assets/images/16x16/baddims0x0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/images/16x16/baddims0x0.png -------------------------------------------------------------------------------- /render/testdata/assets/images/16x16/jeremy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/images/16x16/jeremy.png -------------------------------------------------------------------------------- /render/testdata/assets/images/devfile.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/images/devfile.pdn -------------------------------------------------------------------------------- /render/testdata/assets/images/eyes3x3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/images/eyes3x3.png -------------------------------------------------------------------------------- /render/testdata/assets/images/raw/nonsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/render/testdata/assets/images/raw/nonsheet.png -------------------------------------------------------------------------------- /render/tween.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | // Tween takes two images and returns a set of images tweening 9 | // between the two over some number of frames 10 | func Tween(start image.Image, end image.Image, frames int) []*image.RGBA { 11 | bounds := start.Bounds() 12 | w := bounds.Max.X 13 | h := bounds.Max.Y 14 | 15 | tweened := make([]*image.RGBA, frames+2) 16 | progress := 0.0 17 | inc := 1.0 / float64(len(tweened)-1) 18 | for i := range tweened { 19 | tweened[i] = image.NewRGBA(image.Rect(0, 0, w, h)) 20 | for x := 0; x < w; x++ { 21 | for y := 0; y < h; y++ { 22 | r1, g1, b1, a1 := start.At(x, y).RGBA() 23 | r2, g2, b2, a2 := end.At(x, y).RGBA() 24 | 25 | r1f := float64(r1) * (1 - progress) 26 | g1f := float64(g1) * (1 - progress) 27 | b1f := float64(b1) * (1 - progress) 28 | a1f := float64(a1) * (1 - progress) 29 | 30 | r2f := float64(r2) * progress 31 | g2f := float64(g2) * progress 32 | b2f := float64(b2) * progress 33 | a2f := float64(a2) * progress 34 | 35 | c := color.RGBA64{uint16(r1f + r2f), uint16(g1f + g2f), 36 | uint16(b1f + b2f), uint16(a1f + a2f)} 37 | 38 | tweened[i].Set(x, y, c) 39 | } 40 | } 41 | progress += inc 42 | } 43 | 44 | return tweened 45 | } 46 | -------------------------------------------------------------------------------- /render/tween_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestTween(t *testing.T) { 9 | start := NewColorBox(10, 10, color.RGBA{0, 0, 0, 0}) 10 | end := NewColorBox(10, 10, color.RGBA{255, 255, 255, 255}) 11 | // I didn't expect to have to give frames - 2 here 12 | tween := Tween(start.GetRGBA(), end.GetRGBA(), 254) 13 | for i, rgba := range tween { 14 | c := rgba.At(0, 0) 15 | r, g, b, a := c.RGBA() 16 | // I mean, I can guess that this should be near 255 but 17 | // I had to just jump around to actually find 257 (and I've 18 | // had to do this before, and remember this same experience) 19 | v := uint32(257 * i) 20 | 21 | if r != v || g != v || b != v || a != v { 22 | t.Fatalf("tween color mismatch") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scene.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/oakmound/oak/v4/scene" 7 | "github.com/oakmound/oak/v4/timing" 8 | ) 9 | 10 | // AddScene is shorthand for w.SceneMap.AddScene 11 | func (w *Window) AddScene(name string, s scene.Scene) error { 12 | return w.SceneMap.AddScene(name, s) 13 | } 14 | 15 | func (w *Window) sceneTransition(result *scene.Result) { 16 | if result.Transition != nil { 17 | i := 0 18 | cont := true 19 | frameDelay := timing.FPSToFrameDelay(w.DrawFrameRate) 20 | for cont { 21 | // TODO: Transition should take in the amount of time passed, not number of frames, 22 | // to account for however long the transition itself takes. 23 | cont = result.Transition(w.winBuffers[w.bufferIdx].RGBA(), i) 24 | w.publish() 25 | i++ 26 | time.Sleep(frameDelay) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scene/context.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/oakmound/oak/v4/collision" 7 | "github.com/oakmound/oak/v4/event" 8 | "github.com/oakmound/oak/v4/key" 9 | "github.com/oakmound/oak/v4/render" 10 | ) 11 | 12 | // A Context contains all transient engine components used in a scene, including 13 | // the draw stack, event bus, known event callers, collision trees, keyboard state, 14 | // and a reference to the OS window itself. When a scene ends, modifications made 15 | // to these structures will be reset, excluding window modifications. 16 | type Context struct { 17 | // This context will be canceled when the scene ends 18 | context.Context 19 | 20 | PreviousScene string 21 | SceneInput interface{} 22 | Window Window 23 | 24 | *event.CallerMap 25 | event.Handler 26 | *render.DrawStack 27 | *key.State 28 | 29 | MouseTree *collision.Tree 30 | CollisionTree *collision.Tree 31 | } 32 | 33 | // DoEachFrame is a helper method to call a function on each frame for the duration of this scene. 34 | func (ctx *Context) DoEachFrame(f func()) { 35 | event.GlobalBind(ctx, event.Enter, func(_ event.EnterPayload) event.Response { 36 | f() 37 | return 0 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /scene/context_desktop.go: -------------------------------------------------------------------------------- 1 | //go:build !js && !android && !nooswindow 2 | // +build !js,!android,!nooswindow 3 | 4 | package scene 5 | 6 | import "github.com/oakmound/oak/v4/window" 7 | 8 | type Window = window.Window 9 | -------------------------------------------------------------------------------- /scene/context_other.go: -------------------------------------------------------------------------------- 1 | //go:build js || android || nooswindow 2 | // +build js android nooswindow 3 | 4 | package scene 5 | 6 | import "github.com/oakmound/oak/v4/window" 7 | 8 | type Window = window.App 9 | -------------------------------------------------------------------------------- /scene/delay.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/oakmound/oak/v4/render" 8 | ) 9 | 10 | // DoAfter will execute the given function after some duration. When the scene 11 | // ends, DoAfter will exit without calling f. This call blocks until one of those 12 | // conditions is reached. 13 | func (c *Context) DoAfter(d time.Duration, f func()) { 14 | t := time.NewTimer(d) 15 | defer t.Stop() 16 | select { 17 | case <-t.C: 18 | f() 19 | case <-c.Done(): 20 | } 21 | } 22 | 23 | // DoAfterContext will execute the given function once the passed in context is closed. 24 | // When the scene ends, DoAfterContext will exit without calling f. This call blocks until 25 | // one of those conditions is reached. 26 | func (c *Context) DoAfterContext(ctx context.Context, f func()) { 27 | select { 28 | case <-ctx.Done(): 29 | f() 30 | case <-c.Done(): 31 | } 32 | } 33 | 34 | // DrawForTime draws, and after d, undraws an element 35 | func (c *Context) DrawForTime(r render.Renderable, d time.Duration, layers ...int) error { 36 | _, err := c.DrawStack.Draw(r, layers...) 37 | if err != nil { 38 | return err 39 | } 40 | go func(r render.Renderable, d time.Duration) { 41 | c.DoAfter(d, func() { 42 | r.Undraw() 43 | }) 44 | }(r, d) 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /scene/doc.go: -------------------------------------------------------------------------------- 1 | // Package scene provides definitions for interacting with game loop scenes. 2 | package scene 3 | -------------------------------------------------------------------------------- /scene/example_test.go: -------------------------------------------------------------------------------- 1 | package scene_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/oakmound/oak/v4/scene" 7 | ) 8 | 9 | func ExampleMap_GetCurrent() { 10 | m := scene.NewMap() 11 | sc := scene.Scene{ 12 | Start: func(*scene.Context) { 13 | fmt.Println("Starting screen one") 14 | }, 15 | } 16 | m.AddScene("screen1", sc) 17 | m.CurrentScene = "screen2" 18 | _, ok := m.GetCurrent() 19 | if !ok { 20 | fmt.Println("Screen two did not exist") 21 | } 22 | m.CurrentScene = "screen1" 23 | sc, ok = m.GetCurrent() 24 | if !ok { 25 | fmt.Println("Screen one did not exist") 26 | } else { 27 | sc.Start(&scene.Context{ 28 | PreviousScene: "scene0", 29 | }) 30 | } 31 | // Output: Screen two did not exist 32 | // Starting screen one 33 | } 34 | -------------------------------------------------------------------------------- /scene/scene_test.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func randString() string { 9 | length := rand.Intn(100) 10 | data := make([]byte, length) 11 | for i := range data { 12 | data[i] = byte(rand.Intn(255)) 13 | } 14 | return string(data) 15 | } 16 | 17 | func TestGoTo(t *testing.T) { 18 | tests := 10 19 | for i := 0; i < tests; i++ { 20 | s := randString() 21 | gt := GoTo(s) 22 | s2, result := gt() 23 | if s != s2 { 24 | t.Fatalf("expected goto to return %v, got %v", s, s2) 25 | } 26 | if result != nil { 27 | t.Fatalf("expected goto to return nil result, got %v", result) 28 | } 29 | } 30 | } 31 | 32 | func TestGoToPtr(t *testing.T) { 33 | tests := 10 34 | s := new(string) 35 | gt := GoToPtr(s) 36 | for i := 0; i < tests; i++ { 37 | *s = randString() 38 | s2, result := gt() 39 | if *s != s2 { 40 | t.Fatalf("expected gotoptr to return %v, got %v", *s, s2) 41 | } 42 | if result != nil { 43 | t.Fatalf("expected gotoptr to return nil result, got %v", result) 44 | } 45 | } 46 | } 47 | 48 | func TestGoToPtrNil(t *testing.T) { 49 | s, _ := GoToPtr(nil)() 50 | if s != "" { 51 | t.Fatalf("expected nil gotoptr to return empty string, got %v", s) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scene/transition.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | 7 | "github.com/oakmound/oak/v4/render/mod" 8 | ) 9 | 10 | // Transition functions can be set to occur at the end of a scene. 11 | type Transition func(*image.RGBA, int) bool 12 | 13 | // Zoom transitions by performing a simplistic zoom each frame towards some 14 | // percentage-based part of the screen. 15 | func Zoom(xPerc, yPerc float64, frames int, zoomRate float64) Transition { 16 | return func(buf *image.RGBA, frame int) bool { 17 | if frame > frames { 18 | return false 19 | } 20 | z := mod.Zoom(xPerc, yPerc, 1+zoomRate*float64(frame)) 21 | draw.Draw(buf, buf.Bounds(), z(buf), image.ZP, draw.Src) 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scene/transition_gift.go: -------------------------------------------------------------------------------- 1 | //go:build !nogift 2 | // +build !nogift 3 | 4 | package scene 5 | 6 | import ( 7 | "image" 8 | 9 | "github.com/oakmound/oak/v4/render/mod" 10 | ) 11 | 12 | // Fade is a scene transition that fades to black at a given rate for 13 | // a total of 'frames' frames 14 | func Fade(rate float32, frames int) Transition { 15 | rate *= -1 16 | return func(buf *image.RGBA, frame int) bool { 17 | if frame > frames { 18 | return false 19 | } 20 | i := float32(frame) 21 | mod.Brighten(rate * i)(buf) 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sceneLoop_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/oakmound/oak/v4/scene" 8 | ) 9 | 10 | func TestSceneLoopUnknownScene(t *testing.T) { 11 | c1 := NewWindow() 12 | err := c1.SceneMap.AddScene("blank", scene.Scene{}) 13 | if err != nil { 14 | t.Fatalf("Scene Add failed: %v", err) 15 | } 16 | err = c1.Init("bad") 17 | if err == nil { 18 | t.Fatal("expected error from Init on unknown scene") 19 | } 20 | } 21 | 22 | func TestSceneLoopUnknownErrorScene(t *testing.T) { 23 | c1 := NewWindow() 24 | err := c1.SceneMap.AddScene("blank", scene.Scene{}) 25 | if err != nil { 26 | t.Fatalf("Scene Add failed: %v", err) 27 | } 28 | c1.ErrorScene = "bad2" 29 | err = c1.Init("bad") 30 | if err == nil { 31 | t.Fatal("expected error from Init to error scene") 32 | } 33 | } 34 | 35 | func TestSceneLoopErrorScene(t *testing.T) { 36 | c1 := NewWindow() 37 | err := c1.SceneMap.AddScene("blank", scene.Scene{}) 38 | if err != nil { 39 | t.Fatalf("Scene Add failed: %v", err) 40 | } 41 | c1.ErrorScene = "blank" 42 | go func() { 43 | err = c1.Init("bad") 44 | }() 45 | time.Sleep(2 * time.Second) 46 | if err != nil { 47 | t.Fatalf("error transitioning to unknown scene: %v", err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scene_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/oakmound/oak/v4/oakerr" 9 | "github.com/oakmound/oak/v4/scene" 10 | ) 11 | 12 | func TestSceneTransition(t *testing.T) { 13 | c1 := NewWindow() 14 | c1.AddScene("1", scene.Scene{ 15 | Start: func(context *scene.Context) { 16 | go context.Window.NextScene() 17 | }, 18 | End: func() (nextScene string, result *scene.Result) { 19 | return "2", &scene.Result{ 20 | Transition: scene.Fade(1, 10), 21 | } 22 | }, 23 | }) 24 | c1.AddScene("2", scene.Scene{ 25 | Start: func(context *scene.Context) { 26 | context.Window.Quit() 27 | }, 28 | }) 29 | c1.Init("1") 30 | } 31 | 32 | func TestLoadingSceneClaimed(t *testing.T) { 33 | c1 := NewWindow() 34 | c1.AddScene(oakLoadingScene, scene.Scene{}) 35 | err := c1.Init("1") 36 | var wantErr oakerr.ExistingElement 37 | if !errors.As(err, &wantErr) { 38 | t.Fatalf("expected existing element error, got %v", err) 39 | } 40 | } 41 | 42 | func TestSceneGoTo(t *testing.T) { 43 | c1 := NewWindow() 44 | var cancel func() 45 | c1.ParentContext, cancel = context.WithCancel(c1.ParentContext) 46 | c1.AddScene("1", scene.Scene{ 47 | Start: func(context *scene.Context) { 48 | context.Window.GoToScene("good") 49 | }, 50 | End: func() (nextScene string, result *scene.Result) { 51 | return "bad", &scene.Result{ 52 | Transition: scene.Fade(1, 10), 53 | } 54 | }, 55 | }) 56 | c1.AddScene("good", scene.Scene{ 57 | Start: func(ctx *scene.Context) { 58 | cancel() 59 | }, 60 | }) 61 | c1.Init("1") 62 | } 63 | -------------------------------------------------------------------------------- /screenFilter.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/oakmound/oak/v4/render/mod" 8 | ) 9 | 10 | // SetPalette tells oak to conform the screen to the input color palette before drawing. 11 | func (w *Window) SetPalette(palette color.Palette) { 12 | w.SetDrawFilter(mod.ConformToPalette(palette)) 13 | } 14 | 15 | // SetDrawFilter will filter the screen by the given modification function prior 16 | // to publishing the screen's rgba to be displayed. 17 | func (w *Window) SetDrawFilter(screenFilter mod.Filter) { 18 | w.prePublish = func(buf *image.RGBA) { 19 | screenFilter(buf) 20 | } 21 | } 22 | 23 | // ClearScreenFilter resets the draw function to no longer filter the screen before 24 | // publishing it to the window. 25 | func (w *Window) ClearScreenFilter() { 26 | w.prePublish = func(buf *image.RGBA) {} 27 | } 28 | -------------------------------------------------------------------------------- /screenFilter_test.go: -------------------------------------------------------------------------------- 1 | package oak 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | ) 8 | 9 | func TestScreenFilter(t *testing.T) { 10 | c1 := NewWindow() 11 | blackAndWhite := color.Palette{ 12 | color.RGBA{0, 0, 0, 255}, 13 | color.RGBA{255, 255, 255, 255}, 14 | } 15 | c1.SetPalette(blackAndWhite) 16 | buf := image.NewRGBA(image.Rect(0, 0, 1, 1)) 17 | c1.prePublish(buf) 18 | } 19 | -------------------------------------------------------------------------------- /shake/README.md: -------------------------------------------------------------------------------- 1 | # shake 2 | 3 | The shake package helps one "shake", or quickly move back-and-forth, things. 4 | 5 | One can shake the screen via `shake.Screen(ctx, time.Second)`, or shake anything that has a `ShiftPos` function with 6 | `shake.Shake(...)`. Both of these package functions build on the `shake.Shaker` type, which has some settings for configuring 7 | how a thing is shaken. 8 | -------------------------------------------------------------------------------- /shape/doc.go: -------------------------------------------------------------------------------- 1 | // Package shape provides 2D shaping utilities. 2 | package shape 3 | -------------------------------------------------------------------------------- /shape/points.go: -------------------------------------------------------------------------------- 1 | package shape 2 | 3 | import ( 4 | "github.com/oakmound/oak/v4/alg/intgeom" 5 | ) 6 | 7 | // Points is a shape defined by a set of points. 8 | // It ignores input width and height given to it as it only cares about its points. 9 | type Points map[intgeom.Point2]struct{} 10 | 11 | // NewPoints creates a Points shape from any number of intgeom Points 12 | func NewPoints(ps ...intgeom.Point2) Shape { 13 | points := make(map[intgeom.Point2]struct{}, len(ps)) 14 | for _, p := range ps { 15 | points[p] = struct{}{} 16 | } 17 | return Points(points) 18 | } 19 | 20 | // In returns whether the input x and y are a point in the point map 21 | func (p Points) In(x, y int, sizes ...int) bool { 22 | _, ok := p[intgeom.Point2{x, y}] 23 | return ok 24 | } 25 | 26 | // Outline returns the set of points along the point map's outline, if 27 | // one exists 28 | func (p Points) Outline(sizes ...int) ([]intgeom.Point2, error) { 29 | return ToOutline(p)(sizes...) 30 | } 31 | 32 | // Rect returns a 2D slice of booleans representing the output of the In function in that rectangle 33 | func (p Points) Rect(sizes ...int) [][]bool { 34 | return InToRect(p.In)(sizes...) 35 | } 36 | -------------------------------------------------------------------------------- /shape/points_test.go: -------------------------------------------------------------------------------- 1 | package shape 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oakmound/oak/v4/alg/intgeom" 7 | ) 8 | 9 | var ( 10 | testPoints = NewPoints( 11 | intgeom.Point2{1, 1}, intgeom.Point2{2, 1}, intgeom.Point2{3, 1}, 12 | intgeom.Point2{1, 2}, intgeom.Point2{3, 2}, 13 | intgeom.Point2{1, 3}, intgeom.Point2{2, 3}, intgeom.Point2{3, 3}, 14 | ) 15 | ) 16 | 17 | func TestPointsIn(t *testing.T) { 18 | if !testPoints.In(1, 3, 1, 1) { 19 | t.Fatalf("1,3 was not in testPoints") 20 | } 21 | if testPoints.In(10, 10, 1, 1) { 22 | t.Fatalf("10,10 was in testPoints") 23 | } 24 | } 25 | 26 | func TestPointsOutline(t *testing.T) { 27 | testOutline, _ := testPoints.Outline(4, 4) 28 | if (intgeom.Point2{3, 2}) != testOutline[3] { 29 | t.Fatalf("expected 3,2 at index 3 in outline, was %v", testOutline[3]) 30 | } 31 | } 32 | 33 | func TestPointsRect(t *testing.T) { 34 | testRect := testPoints.Rect(4, 4) 35 | if testRect[0][0] { 36 | t.Fatalf("0,0 should not be set") 37 | } 38 | if testRect[2][2] { 39 | t.Fatalf("2,2 should not be set") 40 | } 41 | if !testRect[1][1] { 42 | t.Fatalf("1,1 should be set") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /shape/rect_test.go: -------------------------------------------------------------------------------- 1 | package shape 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRect(t *testing.T) { 9 | shapes := []Shape{Square, Rectangle, Diamond, Circle, Checkered, Heart, JustIn(NotIn(Diamond.In))} 10 | 11 | w, h := 10, 10 12 | for k, s := range shapes { 13 | r := InToRect(s.In)(w, h) 14 | if !reflect.DeepEqual(r, s.Rect(w, h)) { 15 | t.Fatalf("Shape %v's InToRect did not match s.Rect", k) 16 | } 17 | for i := 0; i < w; i++ { 18 | for j := 0; j < h; j++ { 19 | if r[i][j] != s.In(i, j, w, h) { 20 | t.Fatalf("Shape %v's In at (%d,%d) did not match s.Rect", k, i, j) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | func TestRectangleIn(t *testing.T) { 28 | if Rectangle.In(10, 10, 5, 5) { 29 | t.Fatal("10,10 should not be in a 5x5 rectangle") 30 | } 31 | } 32 | 33 | func TestStrictRect(t *testing.T) { 34 | sr := NewStrictRect(5, 5) 35 | for x := 0; x < 6; x++ { 36 | for y := 0; y < 6; y++ { 37 | if sr.In(x, y) { 38 | t.Fatalf("StrictRect.In was not completely false at %d,%d", x, y) 39 | } 40 | } 41 | } 42 | r := sr.Rect() 43 | for x := 0; x < 5; x++ { 44 | for y := 0; y < 5; y++ { 45 | if r[x][y] { 46 | t.Fatalf("StrictRect.Rect was not completely false at %d,%d", x, y) 47 | } 48 | } 49 | } 50 | 51 | sr[3][3] = true 52 | 53 | out, err := sr.Outline() 54 | if err != nil { 55 | t.Fatalf("expected no error on outline, got %v", err) 56 | } 57 | if len(out) != 1 { 58 | t.Fatalf("expected 1 outline length, got %v", len(out)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /shape/shape.go: -------------------------------------------------------------------------------- 1 | package shape 2 | 3 | import "github.com/oakmound/oak/v4/alg/intgeom" 4 | 5 | // A Shape represents a rectangle of width/height size 6 | // where for each x,y coordinate, either that value lies 7 | // inside the shape or outside of the shape, represented 8 | // by true or false. Shapes can be fuzzed along their border 9 | // to create gradients of floats, and shapes can be queried 10 | // to just produce a 2d boolean array of width/height size. 11 | // Todo: consider if the number of coordinate arguments 12 | // should be variadic, if width/height should not be combined 13 | // and/or variadic, for additional dimension support 14 | type Shape interface { 15 | In(x, y int, sizes ...int) bool 16 | Outline(sizes ...int) ([]intgeom.Point2, error) 17 | Rect(sizes ...int) [][]bool 18 | } 19 | -------------------------------------------------------------------------------- /shiny/AUTHORS: -------------------------------------------------------------------------------- 1 | # Prior to forking, this source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /shiny/CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # Prior to forking, this source code was written by the Go contributors. 2 | # The master list of contributors is in the main Go distribution, 3 | # visible at http://tip.golang.org/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /shiny/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /shiny/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /shiny/README.md: -------------------------------------------------------------------------------- 1 | # Driver 2 | 3 | This is a clone of github.com/oakmound/oak/shiny, itself a fork of golang.org/exp/shiny. 4 | The goal of this fork is to add additional window management functionality, and 5 | focus the project down to just window management / common OS interfaces. 6 | 7 | The goal of the clone here is to reduce iteration time of new features in oak. This 8 | clone may be brought back to github.com/oakmound/oak/shiny regularly, if we make 9 | significant stable improvements. 10 | 11 | ## Long Term Plans 12 | 13 | 1. Standardize interfaces across OSes 14 | 2. Add new drivers for better performance, optional CGO 15 | 3. Add Fullscreen, screen movement, common screen options to all OSes 16 | 4. Add Window scaling types (bicubic, etc.) to all OSes 17 | -------------------------------------------------------------------------------- /shiny/doc.go: -------------------------------------------------------------------------------- 1 | // Package shiny provides interfaces and drivers for instantiating and managing application windows 2 | package shiny 3 | -------------------------------------------------------------------------------- /shiny/driver/androiddriver/image.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | // +build android 3 | 4 | package androiddriver 5 | 6 | import ( 7 | "image" 8 | "sync" 9 | 10 | "golang.org/x/mobile/exp/gl/glutil" 11 | ) 12 | 13 | type imageImpl struct { 14 | screen *Screen 15 | size image.Point 16 | img *glutil.Image 17 | deadLock sync.Mutex 18 | dead bool 19 | } 20 | 21 | func (ii *imageImpl) Size() image.Point { 22 | return ii.size 23 | } 24 | 25 | func (ii *imageImpl) Bounds() image.Rectangle { 26 | return image.Rect(0, 0, ii.size.X, ii.size.Y) 27 | } 28 | 29 | func (ii *imageImpl) Release() { 30 | ii.deadLock.Lock() 31 | ii.img.Release() 32 | ii.dead = true 33 | ii.deadLock.Unlock() 34 | } 35 | 36 | func (ii *imageImpl) RGBA() *image.RGBA { 37 | ii.deadLock.Lock() 38 | if ii.dead { 39 | ii.img = ii.screen.images.NewImage(ii.size.X, ii.size.Y) 40 | ii.dead = false 41 | } 42 | ii.deadLock.Unlock() 43 | return ii.img.RGBA 44 | } 45 | -------------------------------------------------------------------------------- /shiny/driver/androiddriver/texture.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | // +build android 3 | 4 | package androiddriver 5 | 6 | import ( 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | 11 | "github.com/oakmound/oak/v4/shiny/screen" 12 | ) 13 | 14 | type textureImpl struct { 15 | screen *Screen 16 | size image.Point 17 | img *imageImpl 18 | } 19 | 20 | func NewTexture(s *Screen, size image.Point) *textureImpl { 21 | return &textureImpl{ 22 | screen: s, 23 | size: size, 24 | } 25 | } 26 | 27 | func (ti *textureImpl) Size() image.Point { 28 | return ti.size 29 | } 30 | 31 | func (ti *textureImpl) Bounds() image.Rectangle { 32 | return image.Rect(0, 0, ti.size.X, ti.size.Y) 33 | } 34 | 35 | func (ti *textureImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { 36 | ti.img, _ = src.(*imageImpl) 37 | } 38 | func (*textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {} 39 | func (*textureImpl) Release() {} 40 | -------------------------------------------------------------------------------- /shiny/driver/doc.go: -------------------------------------------------------------------------------- 1 | // Package driver exposes screen implementation for various platforms 2 | package driver 3 | -------------------------------------------------------------------------------- /shiny/driver/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package driver provides the default driver for accessing a screen. 6 | package driver 7 | 8 | // TODO: figure out what to say about the responsibility for users of this 9 | // package to check any implicit dependencies' LICENSEs. For example, the 10 | // driver might use third party software outside of golang.org/x, like an X11 11 | // or OpenGL library. 12 | 13 | import ( 14 | "github.com/oakmound/oak/v4/shiny/screen" 15 | ) 16 | 17 | // Main is called by the program's main function to run the graphical 18 | // application. 19 | // 20 | // It calls f on the Screen, possibly in a separate goroutine, as some OS- 21 | // specific libraries require being on 'the main thread'. It returns when f 22 | // returns. 23 | func Main(f func(screen.Screen)) { 24 | main(f) 25 | } 26 | -------------------------------------------------------------------------------- /shiny/driver/driver_android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build android 6 | 7 | //go:build !nooswindow 8 | // +build !nooswindow 9 | 10 | package driver 11 | 12 | import ( 13 | "github.com/oakmound/oak/v4/shiny/driver/androiddriver" 14 | "github.com/oakmound/oak/v4/shiny/screen" 15 | ) 16 | 17 | func main(f func(screen.Screen)) { 18 | androiddriver.Main(f) 19 | } 20 | 21 | type Window = androiddriver.Screen 22 | -------------------------------------------------------------------------------- /shiny/driver/driver_fallback.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !darwin && !linux && !android && !windows && !dragonfly && !openbsd && !nooswindow && !js 6 | // +build !darwin,!linux,!android,!windows,!dragonfly,!openbsd,!nooswindow,!js 7 | 8 | package driver 9 | 10 | import ( 11 | "errors" 12 | 13 | "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" 14 | "github.com/oakmound/oak/v4/shiny/screen" 15 | ) 16 | 17 | func main(f func(screen.Screen)) { 18 | f(errscreen.Stub(errors.New("no driver for accessing a screen"))) 19 | } 20 | 21 | type Window = struct{} 22 | -------------------------------------------------------------------------------- /shiny/driver/driver_js.go: -------------------------------------------------------------------------------- 1 | //go:build js && !nooswindow && !windows && !darwin && !linux 2 | // +build js,!nooswindow,!windows,!darwin,!linux 3 | 4 | package driver 5 | 6 | import ( 7 | "github.com/oakmound/oak/v4/shiny/driver/jsdriver" 8 | "github.com/oakmound/oak/v4/shiny/screen" 9 | ) 10 | 11 | func main(f func(screen.Screen)) { 12 | jsdriver.Main(f) 13 | } 14 | 15 | type Window = jsdriver.Window 16 | -------------------------------------------------------------------------------- /shiny/driver/driver_noop.go: -------------------------------------------------------------------------------- 1 | //go:build nooswindow 2 | // +build nooswindow 3 | 4 | package driver 5 | 6 | import ( 7 | "github.com/oakmound/oak/v4/shiny/driver/noop" 8 | "github.com/oakmound/oak/v4/shiny/screen" 9 | ) 10 | 11 | func main(f func(screen.Screen)) { 12 | noop.Main(f) 13 | } 14 | 15 | type Window = noop.Window 16 | -------------------------------------------------------------------------------- /shiny/driver/driver_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !nooswindow && !android 6 | // +build !nooswindow,!android 7 | 8 | package driver 9 | 10 | import ( 11 | "github.com/oakmound/oak/v4/shiny/driver/windriver" 12 | "github.com/oakmound/oak/v4/shiny/screen" 13 | ) 14 | 15 | func main(f func(screen.Screen)) { 16 | windriver.Main(f) 17 | } 18 | 19 | type Window = windriver.Window 20 | -------------------------------------------------------------------------------- /shiny/driver/driver_x11.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build ((linux && !android) || dragonfly || openbsd) && !nooswindow 6 | // +build linux,!android dragonfly openbsd 7 | // +build !nooswindow 8 | 9 | package driver 10 | 11 | import ( 12 | "github.com/oakmound/oak/v4/shiny/driver/x11driver" 13 | "github.com/oakmound/oak/v4/shiny/screen" 14 | ) 15 | 16 | func main(f func(screen.Screen)) { 17 | x11driver.Main(f) 18 | } 19 | 20 | type Window = x11driver.Window 21 | -------------------------------------------------------------------------------- /shiny/driver/internal/drawer/drawer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package drawer provides functions that help implement screen.Drawer methods. 6 | package drawer 7 | 8 | import ( 9 | "image" 10 | "image/draw" 11 | 12 | "github.com/oakmound/oak/v4/shiny/screen" 13 | "golang.org/x/image/math/f64" 14 | ) 15 | 16 | // Copy implements the Copy method of the screen.Drawer interface by calling 17 | // the Draw method of that same interface. 18 | func Copy(dst screen.SimpleDrawer, dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op) { 19 | dst.Draw(f64.Aff3{ 20 | 1, 0, float64(dp.X - sr.Min.X), 21 | 0, 1, float64(dp.Y - sr.Min.Y), 22 | }, src, sr, op) 23 | } 24 | 25 | // Scale implements the Scale method of the screen.Drawer interface by calling 26 | // the Draw method of that same interface. 27 | func Scale(dst screen.SimpleDrawer, dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { 28 | rx := float64(dr.Dx()) / float64(sr.Dx()) 29 | ry := float64(dr.Dy()) / float64(sr.Dy()) 30 | dst.Draw(f64.Aff3{ 31 | rx, 0, float64(dr.Min.X) - rx*float64(sr.Min.X), 32 | 0, ry, float64(dr.Min.Y) - ry*float64(sr.Min.Y), 33 | }, src, sr, op) 34 | } 35 | -------------------------------------------------------------------------------- /shiny/driver/internal/errscreen/errscreen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package errscreen provides a stub Screen implementation. 6 | package errscreen 7 | 8 | import ( 9 | "image" 10 | 11 | "github.com/oakmound/oak/v4/shiny/screen" 12 | ) 13 | 14 | // Stub returns a Screen whose methods all return the given error. 15 | func Stub(err error) screen.Screen { 16 | return stub{err} 17 | } 18 | 19 | type stub struct { 20 | err error 21 | } 22 | 23 | func (s stub) NewImage(size image.Point) (screen.Image, error) { return nil, s.err } 24 | func (s stub) NewTexture(size image.Point) (screen.Texture, error) { return nil, s.err } 25 | func (s stub) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { return nil, s.err } 26 | -------------------------------------------------------------------------------- /shiny/driver/internal/event/event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package event provides an infinitely buffered double-ended queue of events. 6 | package event 7 | 8 | import ( 9 | "sync" 10 | ) 11 | 12 | // Deque is an infinitely buffered double-ended queue of events. The zero value 13 | // is usable, but a Deque value must not be copied. 14 | type Deque struct { 15 | mu sync.Mutex 16 | cond sync.Cond // cond.L is lazily initialized to &Deque.mu. 17 | back []interface{} // FIFO. 18 | front []interface{} // LIFO. 19 | } 20 | 21 | func (q *Deque) lockAndInit() { 22 | q.mu.Lock() 23 | if q.cond.L == nil { 24 | q.cond.L = &q.mu 25 | } 26 | } 27 | 28 | // NextEvent implements the screen.EventDeque interface. 29 | func (q *Deque) NextEvent() interface{} { 30 | q.lockAndInit() 31 | defer q.mu.Unlock() 32 | 33 | for { 34 | if n := len(q.front); n > 0 { 35 | e := q.front[n-1] 36 | q.front[n-1] = nil 37 | q.front = q.front[:n-1] 38 | return e 39 | } 40 | 41 | if n := len(q.back); n > 0 { 42 | e := q.back[0] 43 | q.back[0] = nil 44 | q.back = q.back[1:] 45 | return e 46 | } 47 | 48 | q.cond.Wait() 49 | } 50 | } 51 | 52 | // Send implements the screen.EventDeque interface. 53 | func (q *Deque) Send(event interface{}) { 54 | q.lockAndInit() 55 | defer q.mu.Unlock() 56 | 57 | q.back = append(q.back, event) 58 | q.cond.Signal() 59 | } 60 | -------------------------------------------------------------------------------- /shiny/driver/internal/swizzle/swizzle_amd64.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package swizzle 6 | 7 | // haveSSSE3 returns whether the CPU supports SSSE3 instructions (i.e. PSHUFB). 8 | // 9 | // Note that this is SSSE3, not SSE3. 10 | func haveSSSE3() bool 11 | 12 | var useBGRA16 = haveSSSE3() 13 | 14 | const useBGRA4 = true 15 | 16 | func bgra16(p []byte) 17 | func bgra4(p []byte) 18 | -------------------------------------------------------------------------------- /shiny/driver/internal/swizzle/swizzle_common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package swizzle provides functions for converting between RGBA pixel 6 | // formats. 7 | package swizzle 8 | 9 | // BGRA converts a pixel buffer between Go's RGBA and other systems' BGRA byte 10 | // orders. 11 | // 12 | // It panics if the input slice length is not a multiple of 4. 13 | func BGRA(p []byte) { 14 | if len(p)%4 != 0 { 15 | panic("input slice length is not a multiple of 4") 16 | } 17 | 18 | // Use asm code for 16- or 4-byte chunks, if supported. 19 | if useBGRA16 { 20 | n := len(p) &^ (16 - 1) 21 | bgra16(p[:n]) 22 | p = p[n:] 23 | } else if useBGRA4 { 24 | bgra4(p) 25 | return 26 | } 27 | 28 | for i := 0; i < len(p); i += 4 { 29 | p[i+0], p[i+2] = p[i+2], p[i+0] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /shiny/driver/internal/swizzle/swizzle_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !amd64 6 | 7 | package swizzle 8 | 9 | const ( 10 | useBGRA16 = false 11 | useBGRA4 = false 12 | ) 13 | 14 | func bgra16(p []byte) { panic("unreachable") } 15 | func bgra4(p []byte) { panic("unreachable") } 16 | -------------------------------------------------------------------------------- /shiny/driver/internal/win32/cursor.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package win32 5 | 6 | import "sync" 7 | 8 | var emptyCursor HCURSOR 9 | var emptyCursorOnce sync.Once 10 | 11 | // Create a custom cursor at run time. 12 | func GetEmptyCursor() HCURSOR { 13 | emptyCursorOnce.Do(func() { 14 | andMASK := []byte{ 15 | 0xFF, 0xFF, 0xFF, 0xFF, 16 | } 17 | 18 | xorMASK := []byte{ 19 | 0x00, 0x00, 0x00, 0x00, 20 | } 21 | emptyCursor = CreateCursor(hThisInstance, // app. instance 22 | 0, // horizontal position of hot spot 23 | 0, // vertical position of hot spot 24 | // 0 width/height is unsupported in testing 25 | 1, // cursor width 26 | 1, // cursor height 27 | andMASK, 28 | xorMASK) 29 | }) 30 | return emptyCursor 31 | } 32 | 33 | // TODO: Add image.Image to cursor conversion and setting functionality 34 | // this can currently be done in oak by having a image follow the cursor around, 35 | // but that will inherently not be as smooth as setting the OS cursor. (but more portable) 36 | -------------------------------------------------------------------------------- /shiny/driver/internal/win32/util.go: -------------------------------------------------------------------------------- 1 | package win32 2 | 3 | import ( 4 | "crypto/rand" 5 | "unsafe" 6 | ) 7 | 8 | func boolToBOOL(value bool) BOOL { 9 | if value { 10 | return 1 11 | } 12 | 13 | return 0 14 | } 15 | 16 | // MakeGUID allocates a GUID from a [16]byte array. If the array 17 | // is uninitialized a random byte array will be used. 18 | func MakeGUID(guid [16]byte) GUID { 19 | if guid == ([16]byte{}) { 20 | rand.Read(guid[:]) 21 | } 22 | // Implementation from hallazzang/go-windows-programming. Would have 23 | // reimplemented but it's so simple that its hard to do so. 24 | return *(*GUID)(unsafe.Pointer(&guid[0])) 25 | } 26 | -------------------------------------------------------------------------------- /shiny/driver/jsdriver/canvas.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package jsdriver 5 | 6 | import ( 7 | "image" 8 | "syscall/js" 9 | ) 10 | 11 | // Adapted from Mark Farnan's go-canvas library (github.com/markfarnan/go-canvas) 12 | type Canvas2D struct { 13 | // DOM properties 14 | window js.Value 15 | doc js.Value 16 | body js.Value 17 | 18 | // Canvas properties 19 | canvas js.Value 20 | ctx js.Value 21 | imgData js.Value 22 | 23 | copybuff js.Value 24 | } 25 | 26 | func NewCanvas2d(width int, height int) *Canvas2D { 27 | var c Canvas2D 28 | c.window = js.Global() 29 | c.doc = c.window.Get("document") 30 | c.body = c.doc.Get("body") 31 | 32 | canvas := c.doc.Call("createElement", "canvas") 33 | 34 | canvas.Set("height", height) 35 | canvas.Set("width", width) 36 | // TODO: screen position 37 | c.body.Call("appendChild", canvas) 38 | 39 | c.canvas = canvas 40 | 41 | // Setup the 2D Drawing context 42 | c.ctx = c.canvas.Call("getContext", "2d", map[string]interface{}{"alpha": false}) 43 | c.imgData = c.ctx.Call("createImageData", width, height) // Note Width, then Height 44 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 45 | c.copybuff = js.Global().Get("Uint8Array").New(len(img.Pix)) // Static JS buffer for copying data out to JS. Defined once and re-used to save on un-needed allocations 46 | 47 | return &c 48 | } 49 | -------------------------------------------------------------------------------- /shiny/driver/jsdriver/image.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package jsdriver 5 | 6 | import "image" 7 | 8 | type imageImpl struct { 9 | screen *screenImpl 10 | size image.Point 11 | rgba *image.RGBA 12 | } 13 | 14 | func (ii imageImpl) Size() image.Point { 15 | return ii.size 16 | } 17 | 18 | func (ii imageImpl) Bounds() image.Rectangle { 19 | return image.Rect(0, 0, ii.size.X, ii.size.Y) 20 | } 21 | 22 | func (imageImpl) Release() {} 23 | 24 | func (ii imageImpl) RGBA() *image.RGBA { 25 | return ii.rgba 26 | } 27 | -------------------------------------------------------------------------------- /shiny/driver/jsdriver/texture.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package jsdriver 5 | 6 | import ( 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | 11 | "github.com/oakmound/oak/v4/shiny/screen" 12 | ) 13 | 14 | type textureImpl struct { 15 | screen *screenImpl 16 | size image.Point 17 | rgba *image.RGBA 18 | } 19 | 20 | func (ti *textureImpl) Size() image.Point { 21 | return ti.size 22 | } 23 | 24 | func (ti *textureImpl) Bounds() image.Rectangle { 25 | return image.Rect(0, 0, ti.size.X, ti.size.Y) 26 | } 27 | 28 | func (ti *textureImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { 29 | rgba := src.RGBA() 30 | ti.rgba = rgba 31 | } 32 | func (*textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {} 33 | func (*textureImpl) Release() {} 34 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin 6 | // +build darwin 7 | 8 | package mtldriver 9 | 10 | import "image" 11 | 12 | // bufferImpl implements screen.Buffer. 13 | type bufferImpl struct { 14 | rgba *image.RGBA 15 | } 16 | 17 | func (*bufferImpl) Release() {} 18 | func (b *bufferImpl) Size() image.Point { return b.rgba.Rect.Max } 19 | func (b *bufferImpl) Bounds() image.Rectangle { return b.rgba.Rect } 20 | func (b *bufferImpl) RGBA() *image.RGBA { return b.rgba } 21 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/internal/appkit/appkit.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin 6 | 7 | void * Window_ContentView(void * window); 8 | 9 | void View_SetLayer(void * view, void * layer); 10 | void View_SetWantsLayer(void * view, bool wantsLayer); 11 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/internal/appkit/appkit.m: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin 6 | 7 | #import 8 | #include "appkit.h" 9 | 10 | void * Window_ContentView(void * window) { 11 | return ((NSWindow *)window).contentView; 12 | } 13 | 14 | void View_SetLayer(void * view, void * layer) { 15 | ((NSView *)view).layer = (CALayer *)layer; 16 | } 17 | 18 | void View_SetWantsLayer(void * view, bool wantsLayer) { 19 | ((NSView *)view).wantsLayer = wantsLayer; 20 | } 21 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/internal/coreanim/coreanim.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin 6 | 7 | typedef unsigned long uint_t; 8 | typedef unsigned short uint16_t; 9 | 10 | void * MakeMetalLayer(); 11 | 12 | uint16_t MetalLayer_PixelFormat(void * metalLayer); 13 | void MetalLayer_SetDevice(void * metalLayer, void * device); 14 | const char * MetalLayer_SetPixelFormat(void * metalLayer, uint16_t pixelFormat); 15 | const char * MetalLayer_SetMaximumDrawableCount(void * metalLayer, uint_t maximumDrawableCount); 16 | void MetalLayer_SetDisplaySyncEnabled(void * metalLayer, bool displaySyncEnabled); 17 | void MetalLayer_SetDrawableSize(void * metalLayer, double width, double height); 18 | void * MetalLayer_NextDrawable(void * metalLayer); 19 | 20 | void * MetalDrawable_Texture(void * drawable); 21 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/screen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin 6 | // +build darwin 7 | 8 | package mtldriver 9 | 10 | import ( 11 | "image" 12 | 13 | "github.com/go-gl/glfw/v3.3/glfw" 14 | "github.com/oakmound/oak/v4/shiny/screen" 15 | ) 16 | 17 | // screenImpl implements screen.Screen. 18 | type screenImpl struct { 19 | newWindowCh chan newWindowReq 20 | } 21 | 22 | func (*screenImpl) NewImage(size image.Point) (screen.Image, error) { 23 | return &bufferImpl{ 24 | rgba: image.NewRGBA(image.Rectangle{Max: size}), 25 | }, nil 26 | } 27 | 28 | func (*screenImpl) NewTexture(size image.Point) (screen.Texture, error) { 29 | return &textureImpl{ 30 | rgba: image.NewRGBA(image.Rectangle{Max: size}), 31 | }, nil 32 | } 33 | 34 | func (s *screenImpl) NewWindow(opts screen.WindowGenerator) (screen.Window, error) { 35 | respCh := make(chan newWindowResp) 36 | s.newWindowCh <- newWindowReq{ 37 | opts: opts, 38 | respCh: respCh, 39 | } 40 | glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on newWindowCh. 41 | resp := <-respCh 42 | return resp.w, resp.err 43 | } 44 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/texture.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin 6 | // +build darwin 7 | 8 | package mtldriver 9 | 10 | import ( 11 | "image" 12 | "image/color" 13 | 14 | "github.com/oakmound/oak/v4/shiny/screen" 15 | "golang.org/x/image/draw" 16 | ) 17 | 18 | // textureImpl implements screen.Texture. 19 | type textureImpl struct { 20 | rgba *image.RGBA 21 | } 22 | 23 | func (*textureImpl) Release() {} 24 | func (t *textureImpl) Size() image.Point { return t.rgba.Rect.Max } 25 | func (t *textureImpl) Bounds() image.Rectangle { return t.rgba.Rect } 26 | 27 | func (t *textureImpl) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { 28 | draw.Draw(t.rgba, sr.Sub(sr.Min).Add(dp), src.RGBA(), sr.Min, draw.Src) 29 | } 30 | 31 | func (t *textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) { 32 | draw.Draw(t.rgba, dr, &image.Uniform{src}, image.Point{}, op) 33 | } 34 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/window_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && amd64 2 | // +build darwin,amd64 3 | 4 | package mtldriver 5 | 6 | import ( 7 | "image" 8 | 9 | "github.com/oakmound/oak/v4/shiny/driver/internal/drawer" 10 | "github.com/oakmound/oak/v4/shiny/screen" 11 | "golang.org/x/image/draw" 12 | "golang.org/x/image/math/f64" 13 | ) 14 | 15 | func (w *Window) Upload(dp image.Point, src screen.Image, sr image.Rectangle) { 16 | draw.Draw(w.bgra, sr.Sub(sr.Min).Add(dp), src.RGBA(), sr.Min, draw.Src) 17 | } 18 | 19 | func (w *Window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { 20 | draw.NearestNeighbor.Transform(w.bgra, src2dst, src.(*textureImpl).rgba, sr, op, nil) 21 | } 22 | 23 | func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { 24 | drawer.Scale(w, dr, src, sr, op) 25 | } 26 | 27 | type BGRA = image.RGBA 28 | 29 | var NewBGRA = image.NewRGBA 30 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver/window_arm64.go: -------------------------------------------------------------------------------- 1 | //go:build arm64 && darwin 2 | // +build arm64,darwin 3 | 4 | package mtldriver 5 | 6 | import ( 7 | "image" 8 | 9 | "github.com/oakmound/oak/v4/shiny/driver/internal/drawer" 10 | "github.com/oakmound/oak/v4/shiny/screen" 11 | "golang.org/x/image/draw" 12 | "golang.org/x/image/math/f64" 13 | ) 14 | 15 | func (w *Window) Upload(dp image.Point, srcImg screen.Image, sr image.Rectangle) { 16 | dst := w.bgra 17 | r := sr.Sub(sr.Min).Add(dp) 18 | src := srcImg.RGBA() 19 | sp := sr.Min 20 | clip(dst, &r, src, &sp, nil, &image.Point{}) 21 | if r.Empty() { 22 | return 23 | } 24 | 25 | i0 := (r.Min.X - dst.Rect.Min.X) * 4 26 | i1 := (r.Max.X - dst.Rect.Min.X) * 4 27 | si0 := (sp.X - src.Rect.Min.X) * 4 28 | yMax := r.Max.Y - dst.Rect.Min.Y 29 | 30 | y := r.Min.Y - dst.Rect.Min.Y 31 | sy := sp.Y - src.Rect.Min.Y 32 | for ; y != yMax; y, sy = y+1, sy+1 { 33 | dpix := dst.Pix[y*dst.Stride:] 34 | spix := src.Pix[sy*src.Stride:] 35 | 36 | for i, si := i0, si0; i < i1; i, si = i+4, si+4 { 37 | s := spix[si : si+4 : si+4] // Small cap improves performance, see https://golang.org/issue/27857 38 | d := dpix[i : i+4 : i+4] 39 | d[0] = s[2] 40 | d[1] = s[1] 41 | d[2] = s[0] 42 | d[3] = s[3] 43 | } 44 | } 45 | } 46 | 47 | func (w *Window) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op) { 48 | nnInterpolator{}.Transform(w.bgra, src2dst, src.(*textureImpl).rgba, sr) 49 | } 50 | 51 | func (w *Window) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op) { 52 | drawer.Scale(w, dr, src, sr, draw.Over) 53 | } 54 | -------------------------------------------------------------------------------- /shiny/driver/mtldriver_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin && !noop 6 | // +build darwin,!noop 7 | 8 | package driver 9 | 10 | import ( 11 | "github.com/oakmound/oak/v4/shiny/driver/mtldriver" 12 | "github.com/oakmound/oak/v4/shiny/screen" 13 | ) 14 | 15 | func main(f func(screen.Screen)) { 16 | mtldriver.Main(f) 17 | } 18 | 19 | type Window = mtldriver.Window 20 | -------------------------------------------------------------------------------- /shiny/driver/windriver/other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !windows 6 | // +build !windows 7 | 8 | package windriver 9 | 10 | import ( 11 | "fmt" 12 | "runtime" 13 | 14 | "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" 15 | "github.com/oakmound/oak/v4/shiny/screen" 16 | ) 17 | 18 | // Main is called by the program's main function to run the graphical 19 | // application. 20 | // 21 | // It calls f on the Screen, possibly in a separate goroutine, as some OS- 22 | // specific libraries require being on 'the main thread'. It returns when f 23 | // returns. 24 | func Main(f func(screen.Screen)) { 25 | f(errscreen.Stub(fmt.Errorf( 26 | "windriver: unsupported GOOS/GOARCH %s/%s", runtime.GOOS, runtime.GOARCH))) 27 | } 28 | -------------------------------------------------------------------------------- /shiny/driver/windriver/syscall.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go 6 | 7 | package windriver 8 | -------------------------------------------------------------------------------- /shiny/driver/windriver/windriver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package windriver 9 | 10 | import ( 11 | "github.com/oakmound/oak/v4/shiny/driver/internal/errscreen" 12 | "github.com/oakmound/oak/v4/shiny/driver/internal/win32" 13 | "github.com/oakmound/oak/v4/shiny/screen" 14 | ) 15 | 16 | // Main is called by the program's main function to run the graphical 17 | // application. 18 | // 19 | // It calls f on the Screen, possibly in a separate goroutine, as some OS- 20 | // specific libraries require being on 'the main thread'. It returns when f 21 | // returns. 22 | func Main(f func(screen.Screen)) { 23 | screenHWND, err := win32.NewScreen() 24 | if err != nil { 25 | f(errscreen.Stub(err)) 26 | return 27 | } 28 | screen := newScreen(screenHWND) 29 | if err := win32.Main(screenHWND, func() { f(screen) }); err != nil { 30 | f(errscreen.Stub(err)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shiny/driver/x11driver/shm_linux_ipc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build linux 6 | // +build 386 ppc64 ppc64le s390x 7 | 8 | package x11driver 9 | 10 | import ( 11 | "fmt" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | // These constants are from /usr/include/linux/ipc.h 17 | const ( 18 | ipcPrivate = 0 19 | ipcRmID = 0 20 | 21 | shmAt = 21 22 | shmDt = 22 23 | shmGet = 23 24 | shmCtl = 24 25 | ) 26 | 27 | func shmOpen(size int) (shmid uintptr, addr unsafe.Pointer, err error) { 28 | shmid, _, errno0 := syscall.RawSyscall6(syscall.SYS_IPC, shmGet, ipcPrivate, uintptr(size), 0600, 0, 0) 29 | if errno0 != 0 { 30 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmget: %v", errno0) 31 | } 32 | _, _, errno1 := syscall.RawSyscall6(syscall.SYS_IPC, shmAt, shmid, 0, uintptr(unsafe.Pointer(&addr)), 0, 0) 33 | _, _, errno2 := syscall.RawSyscall6(syscall.SYS_IPC, shmCtl, shmid, ipcRmID, 0, 0, 0) 34 | if errno1 != 0 { 35 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmat: %v", errno1) 36 | } 37 | if errno2 != 0 { 38 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmctl: %v", errno2) 39 | } 40 | return shmid, addr, nil 41 | } 42 | 43 | func shmClose(p unsafe.Pointer) error { 44 | _, _, errno := syscall.RawSyscall6(syscall.SYS_IPC, shmDt, 0, 0, 0, uintptr(p), 0) 45 | if errno != 0 { 46 | return fmt.Errorf("shmdt: %v", errno) 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /shiny/driver/x11driver/shm_openbsd_syscall.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build openbsd 6 | // +build i386 amd64 7 | 8 | package x11driver 9 | 10 | import ( 11 | "fmt" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | // These constants are from /usr/include/sys/ipc.h 17 | const ( 18 | ipcPrivate = 0 19 | ipcRmID = 0 20 | ) 21 | 22 | func shmOpen(size int) (shmid uintptr, addr unsafe.Pointer, err error) { 23 | shmid, _, errno0 := syscall.RawSyscall(syscall.SYS_SHMGET, ipcPrivate, uintptr(size), 0666) 24 | if errno0 != 0 { 25 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmget: %v", errno0) 26 | } 27 | p, _, errno1 := syscall.RawSyscall(syscall.SYS_SHMAT, shmid, 0, 0) 28 | _, _, errno2 := syscall.RawSyscall(syscall.SYS_SHMCTL, shmid, ipcRmID, 0) 29 | if errno1 != 0 { 30 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmat: %v", errno1) 31 | } 32 | if errno2 != 0 { 33 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmctl: %v", errno2) 34 | } 35 | return shmid, unsafe.Pointer(p), nil 36 | } 37 | 38 | func shmClose(p unsafe.Pointer) error { 39 | _, _, errno := syscall.RawSyscall(syscall.SYS_SHMDT, uintptr(p), 0, 0) 40 | if errno != 0 { 41 | return fmt.Errorf("shmdt: %v", errno) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /shiny/driver/x11driver/shm_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !linux 6 | // +build !dragonfly 7 | // +build !openbsd 8 | 9 | package x11driver 10 | 11 | import ( 12 | "fmt" 13 | "runtime" 14 | "unsafe" 15 | ) 16 | 17 | func shmOpen(size int) (shmid uintptr, addr unsafe.Pointer, err error) { 18 | return 0, unsafe.Pointer(uintptr(0)), 19 | fmt.Errorf("unsupported GOOS/GOARCH %s/%s", runtime.GOOS, runtime.GOARCH) 20 | } 21 | 22 | func shmClose(p unsafe.Pointer) error { 23 | return fmt.Errorf("unsupported GOOS/GOARCH %s/%s", runtime.GOOS, runtime.GOARCH) 24 | } 25 | -------------------------------------------------------------------------------- /shiny/driver/x11driver/shm_shmopen_syscall.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build linux dragonfly 6 | // +build amd64 arm arm64 mips64 mips64le 7 | 8 | package x11driver 9 | 10 | import ( 11 | "fmt" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | // These constants are from /usr/include/linux/ipc.h 17 | const ( 18 | ipcPrivate = 0 19 | ipcRmID = 0 20 | ) 21 | 22 | func shmOpen(size int) (shmid uintptr, addr unsafe.Pointer, err error) { 23 | shmid, _, errno0 := syscall.RawSyscall(syscall.SYS_SHMGET, ipcPrivate, uintptr(size), 0600) 24 | if errno0 != 0 { 25 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmget: %v", errno0) 26 | } 27 | p, _, errno1 := syscall.RawSyscall(syscall.SYS_SHMAT, shmid, 0, 0) 28 | _, _, errno2 := syscall.RawSyscall(syscall.SYS_SHMCTL, shmid, ipcRmID, 0) 29 | if errno1 != 0 { 30 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmat: %v", errno1) 31 | } 32 | if errno2 != 0 { 33 | return 0, unsafe.Pointer(uintptr(0)), fmt.Errorf("shmctl: %v", errno2) 34 | } 35 | return shmid, unsafe.Pointer(p), nil 36 | } 37 | 38 | func shmClose(p unsafe.Pointer) error { 39 | _, _, errno := syscall.RawSyscall(syscall.SYS_SHMDT, uintptr(p), 0, 0) 40 | if errno != 0 { 41 | return fmt.Errorf("shmdt: %v", errno) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /shiny/screen/event.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | // EventDeque is an infinitely buffered double-ended queue of events. 4 | type EventDeque interface { 5 | // Send adds an event to the end of the deque. They are returned by 6 | // NextEvent in FIFO order. 7 | Send(event interface{}) 8 | 9 | // NextEvent returns the next event in the deque. It blocks until such an 10 | // event has been sent. 11 | // 12 | // Typical event types include: 13 | // - lifecycle.Event 14 | // - size.Event 15 | // - paint.Event 16 | // - key.Event 17 | // - mouse.Event 18 | // - touch.Event 19 | // from the golang.org/x/mobile/event/... packages. Other packages may send 20 | // events, of those types above or of other types, via Send. 21 | NextEvent() interface{} 22 | } 23 | -------------------------------------------------------------------------------- /shiny/screen/spanner.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import "image" 4 | 5 | // Spanner types span some distance. This distance can either be returned as 6 | // a rectangle from 0,0 or as a size point. 7 | type Spanner interface { 8 | // Size returns the size of this Spanner. 9 | Size() image.Point 10 | 11 | // Bounds returns the bounds of this Spanner's span. 12 | Bounds() image.Rectangle 13 | } 14 | -------------------------------------------------------------------------------- /shiny/screen/utf.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import "unicode/utf8" 4 | 5 | func sanitizeUTF8(s string, n int) string { 6 | if n < len(s) { 7 | s = s[:n] 8 | } 9 | i := 0 10 | for i < len(s) { 11 | r, n := utf8.DecodeRuneInString(s[i:]) 12 | if r == 0 || (r == utf8.RuneError && n == 1) { 13 | break 14 | } 15 | i += n 16 | } 17 | return s[:i] 18 | } 19 | -------------------------------------------------------------------------------- /shiny/screen/utf_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package screen 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestSanitizeUTF8(t *testing.T) { 12 | const n = 8 13 | 14 | testCases := []struct { 15 | s, want string 16 | }{ 17 | {"", ""}, 18 | {"a", "a"}, 19 | {"a\x00", "a"}, 20 | {"a\x80", "a"}, 21 | {"\x00a", ""}, 22 | {"\x80a", ""}, 23 | {"abc", "abc"}, 24 | {"foo b\x00r qux", "foo b"}, 25 | {"foo b\x80r qux", "foo b"}, 26 | {"foo b\xffr qux", "foo b"}, 27 | 28 | // "\xc3\xa0" is U+00E0 LATIN SMALL LETTER A WITH GRAVE. 29 | {"\xc3\xa0pqrs", "\u00e0pqrs"}, 30 | {"a\xc3\xa0pqrs", "a\u00e0pqrs"}, 31 | {"ab\xc3\xa0pqrs", "ab\u00e0pqrs"}, 32 | {"abc\xc3\xa0pqrs", "abc\u00e0pqr"}, 33 | {"abcd\xc3\xa0pqrs", "abcd\u00e0pq"}, 34 | {"abcde\xc3\xa0pqrs", "abcde\u00e0p"}, 35 | {"abcdef\xc3\xa0pqrs", "abcdef\u00e0"}, 36 | {"abcdefg\xc3\xa0pqrs", "abcdefg"}, 37 | {"abcdefgh\xc3\xa0pqrs", "abcdefgh"}, 38 | {"abcdefghi\xc3\xa0pqrs", "abcdefgh"}, 39 | {"abcdefghij\xc3\xa0pqrs", "abcdefgh"}, 40 | 41 | // "世" is "\xe4\xb8\x96". 42 | // "界" is "\xe7\x95\x8c". 43 | {"H 世界", "H 世界"}, 44 | {"Hi 世界", "Hi 世"}, 45 | {"Hello 世界", "Hello "}, 46 | } 47 | 48 | for _, tc := range testCases { 49 | if got := sanitizeUTF8(tc.s, n); got != tc.want { 50 | t.Errorf("sanitizeUTF8(%q): got %q, want %q", tc.s, got, tc.want) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test_examples.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | examples=$(find ./examples | grep main.go$) 4 | root=$(pwd) 5 | for ex in $examples 6 | do 7 | echo $ex 8 | dir=$(dirname $ex) 9 | cd $dir 10 | timeout 10 go run $(basename $ex) 11 | retVal=$? 12 | echo "exit status" $retVal 13 | if [ $retVal -ne 124 ]; then 14 | exit 1 15 | fi 16 | cd $root 17 | done -------------------------------------------------------------------------------- /test_examples_js.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | examples=$(find ./examples | grep main.go$) 6 | root=$(pwd) 7 | for ex in $examples 8 | do 9 | echo "$ex" 10 | dir=$(dirname "$ex") 11 | # excluded because screenopts explicitly demonstrates 12 | # desktop-specific features 13 | if [[ "$dir" == "./examples/screenopts" ]]; then 14 | continue 15 | fi 16 | # excluded because text includes a find-font dependency 17 | # that does not compile in js 18 | if [[ "$dir" == "./examples/text" ]]; then 19 | continue 20 | fi 21 | cd "$dir" 22 | GOOS=js GOARCH=wasm go build . 23 | cd "$root" 24 | done -------------------------------------------------------------------------------- /testdata/audio/placeholder.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/testdata/audio/placeholder.txt -------------------------------------------------------------------------------- /testdata/default.config: -------------------------------------------------------------------------------- 1 | { 2 | "assets": { 3 | "imagePath":"assets/images/", 4 | "audioPath":"assets/audio/" 5 | }, 6 | 7 | "screen":{ 8 | "X":0, 9 | "Y":0, 10 | "width": 640, 11 | "height": 480, 12 | "scale": 1 13 | }, 14 | 15 | "debug":{ 16 | "level": "ERROR", 17 | "filter": "" 18 | }, 19 | "frameRate": 60, 20 | "drawFrameRate": 60, 21 | "idleDrawFrameRate": 60, 22 | "language": "English", 23 | "title": "Oak Window", 24 | "batchLoad": false, 25 | "gestureSupport": false 26 | } -------------------------------------------------------------------------------- /testdata/images/placeholder.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/testdata/images/placeholder.txt -------------------------------------------------------------------------------- /testdata/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oakmound/oak/f41daa33212116a56d5d58e5187421d3c621ea51/testdata/screenshot.png -------------------------------------------------------------------------------- /tidy_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go mod tidy 4 | 5 | cd examples/text 6 | go mod tidy -------------------------------------------------------------------------------- /timing/README.md: -------------------------------------------------------------------------------- 1 | # timing 2 | 3 | The timing package contains helper functions for working with frames-per-second time counts. 4 | -------------------------------------------------------------------------------- /timing/fps.go: -------------------------------------------------------------------------------- 1 | // Package timing provides utilities for time. 2 | package timing 3 | 4 | import ( 5 | "math" 6 | "time" 7 | ) 8 | 9 | const ( 10 | nanoPerSecond = 1000000000 11 | 12 | maximumFPS = 1200 13 | ) 14 | 15 | // FPS returns the number of frames being processed per second, 16 | // supposing a time interval from lastTime to now. 17 | func FPS(lastTime, now time.Time) float64 { 18 | fps := 1 / now.Sub(lastTime).Seconds() 19 | // This indicates that we recorded two times within 20 | // the inaccuracy of the OS's system clock, so the values 21 | // were the same. 1200 is chosen because on windows, 22 | // fps will be 1200 instead of a negative value. 23 | if int(fps) < 0 { 24 | return maximumFPS 25 | } 26 | return fps 27 | } 28 | 29 | // FPSToNano converts a framesPerSecond value to the number of 30 | // nanoseconds that should take place for each frame. 31 | func FPSToNano(fps float64) int64 { 32 | if fps == 0.0 { 33 | return math.MaxInt64 34 | } 35 | return int64(nanoPerSecond / fps) 36 | } 37 | 38 | // FPSToFrameDelay converts a frameRate like 60fps into a delay time between frames 39 | func FPSToFrameDelay(frameRate int) time.Duration { 40 | if frameRate == 0 { 41 | return time.Duration(math.MaxInt64) 42 | } 43 | return time.Second / time.Duration(int64(frameRate)) 44 | } 45 | 46 | // FrameDelayToFPS converts a duration of delay between frames into a frames per second count 47 | func FrameDelayToFPS(dur time.Duration) float64 { 48 | if dur == 0 { 49 | return math.MaxFloat64 50 | } 51 | return float64(time.Second) / float64(dur) 52 | } 53 | -------------------------------------------------------------------------------- /window/README.md: -------------------------------------------------------------------------------- 1 | # window 2 | 3 | The window package a common interface for oak-created windows. It essentially exists as a way for `oak` and `scene` to depend on one another without causing an import loop. 4 | --------------------------------------------------------------------------------