diff --git a/README.md b/README.md index 2c46a62..b56c419 100644 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ -# go-BitmapLogicSimulator \ No newline at end of file +# go-BitmapLogicSimulator + +This is an implementation of Bitmap Logic Simulator. +Original article is [https://realhet.wordpress.com/2015/09/02/bitmap-logic-simulator/](https://realhet.wordpress.com/2015/09/02/bitmap-logic-simulator/). + +The application is implemented with OpenGL. + +## How does it work? +### Rules +Usage is very simple. Draw circuit in your favorate image editor, and load the image! + +The rules are also simple. + +``` +'.' = dark pixel, insulation +'#' = bright pixel, conductive + +'bright' means the value of either red or green or blue is greater than 223. 'dark' is the other color. +So color can be various. +``` + +``` +........... +.#########. +........... +.####...... +....######. +........... +some wires +``` + +``` +........... +.....#..... +.....#..... +.####.####. +.....#..... +.....#..... +........... +wire crossing +``` + +``` +........... +....##..... +.####.####. +....##..... +........... +NOT gate (Input is left and output is right. Four directions are possible.) +``` + +So, there is 5 possible certain components. +AND gate can be constructed like this. + +``` +.......... +..##...... +.##.##.... +..##.##... +.....#.##. +..##.##... +.##.##.... +..##...... +.......... +``` + +OR gate can be constructed using two wires merged. + +### Simulation + + diff --git a/circuit.go b/circuit.go new file mode 100644 index 0000000..47e17c3 --- /dev/null +++ b/circuit.go @@ -0,0 +1,4 @@ +package gobls + +type Circuit struct { +} diff --git a/cmd/BitmapLogicSimulator/main.go b/cmd/BitmapLogicSimulator/main.go new file mode 100644 index 0000000..5e10a76 --- /dev/null +++ b/cmd/BitmapLogicSimulator/main.go @@ -0,0 +1,382 @@ +package main + +import ( + "errors" + "fmt" + "image" + "image/draw" + _ "image/png" + "log" + "os" + "runtime" + "strings" + + "github.com/go-gl/gl/v4.1-core/gl" + "github.com/go-gl/glfw/v3.2/glfw" + "github.com/go-gl/mathgl/mgl32" + "github.com/rlj1202/go-BitmapLogicSimulator" +) + +const ( + vertexShader = ` + #version 410 + + uniform mat4 projection; + uniform mat4 scale; + uniform mat4 cameraLoc; + + layout (location = 0) in vec2 position; + layout (location = 1) in vec2 texCoord; + + out vec2 fragTexCoord; + + void main() { + gl_Position = projection * scale * cameraLoc * vec4(position, 0, 1); + fragTexCoord = texCoord; + } + ` + + fragmentShader = ` + #version 410 + + uniform sampler2D tex; + uniform sampler2D tex2; + + in vec2 fragTexCoord; + + out vec4 color; + + void main() { + vec4 a = texture2D(tex, fragTexCoord); + vec4 b = texture2D(tex2, fragTexCoord); + + color = mix(a, b, 0.7); + } + ` +) + +var simulator *gobls.Simulator + +var programId uint32 + +var overlayPBO uint32 +var overlayTex uint32 + +var cameraZoom float32 +var cameraX float32 +var cameraY float32 + +var MMB bool +var prevCursorXPos float64 +var prevCursorYPos float64 + +func main() { + runtime.LockOSThread() + + err := glfw.Init() + if err != nil { + panic(err) + } + defer glfw.Terminate() + + width := 800 + height := 600 + title := "Test window" + + glfw.WindowHint(glfw.Resizable, glfw.True) + glfw.WindowHint(glfw.ContextVersionMajor, 4) + glfw.WindowHint(glfw.ContextVersionMinor, 1) + glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) + glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) + window, err := glfw.CreateWindow(width, height, title, nil, nil) + if err != nil { + panic(err) + } + + window.SetSizeCallback(sizeCallback) + window.SetScrollCallback(scrollCallback) + window.SetMouseButtonCallback(mouseButtonCallback) + window.SetCursorPosCallback(cursorPosCallback) + + window.MakeContextCurrent() + glfw.SwapInterval(1) + + err = gl.Init() + if err != nil { + panic(err) + } + + version := gl.GoStr(gl.GetString(gl.VERSION)) + log.Printf("OpenGL version : %s\n", version) + + vertexShaderId, err := loadShader(vertexShader, gl.VERTEX_SHADER) + if err != nil { + panic(err) + } + fragmentShaderId, err := loadShader(fragmentShader, gl.FRAGMENT_SHADER) + if err != nil { + panic(err) + } + + programId = gl.CreateProgram() + gl.AttachShader(programId, vertexShaderId) + gl.AttachShader(programId, fragmentShaderId) + gl.LinkProgram(programId) + gl.UseProgram(programId) + + texLoc := gl.GetUniformLocation(programId, gl.Str("tex\x00")) + gl.Uniform1i(texLoc, 0) + tex2Loc := gl.GetUniformLocation(programId, gl.Str("tex2\x00")) + gl.Uniform1i(tex2Loc, 1) + + imgFile, err := os.Open("../../test.png") + if err != nil { + panic(err) + } + img, _, err := image.Decode(imgFile) + if err != nil { + fmt.Println("test") + panic(err) + } + texId, err := loadTexture(img) + if err != nil { + panic(err) + } + + // create PBO + gl.GenBuffers(1, &overlayPBO) + gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, overlayPBO) + gl.BufferData(gl.PIXEL_UNPACK_BUFFER, 512*512*4, nil, gl.DYNAMIC_DRAW) + gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, 0) + + // create overlay texture + gl.GenTextures(1, &overlayTex) + gl.BindTexture(gl.TEXTURE_2D, overlayTex) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil) + gl.BindTexture(gl.TEXTURE_2D, 0) + + positions := []float32{ + -0.5, 0.5, + 0.5, 0.5, + 0.5, -0.5, + -0.5, 0.5, + 0.5, -0.5, + -0.5, -0.5, + } + texCoords := []float32{ + 0, 0, + 1, 0, + 1, 1, + 0, 0, + 1, 1, + 0, 1, + } + + vao := uint32(0) + gl.GenVertexArrays(1, &vao) + gl.BindVertexArray(vao) + + positionBuf := uint32(0) + gl.GenBuffers(1, &positionBuf) + gl.BindBuffer(gl.ARRAY_BUFFER, positionBuf) + gl.BufferData(gl.ARRAY_BUFFER, 4*len(positions), gl.Ptr(positions), gl.STATIC_DRAW) + gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 0, nil) + gl.EnableVertexAttribArray(0) + + texCoordBuf := uint32(0) + gl.GenBuffers(1, &texCoordBuf) + gl.BindBuffer(gl.ARRAY_BUFFER, texCoordBuf) + gl.BufferData(gl.ARRAY_BUFFER, 4*len(texCoords), gl.Ptr(texCoords), gl.STATIC_DRAW) + gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 0, nil) + gl.EnableVertexAttribArray(1) + + cameraZoom = 1.0 + updateScaleMat(512, 512) + updateCameraLocMat() + setProjectionMat(programId, float32(width), float32(height)/float32(width)) + + gl.ClearColor(1, 0, 0, 1) + + simulator = gobls.NewSimulator() + simulator.LoadImage(img) + + for !window.ShouldClose() { + glfw.PollEvents() + + gl.Clear(gl.COLOR_BUFFER_BIT) + + updateOverlayTex() + + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, texId) + gl.ActiveTexture(gl.TEXTURE1) + gl.BindTexture(gl.TEXTURE_2D, overlayTex) + + gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 6) + + for i := 0; i < 5; i++ { + simulator.Simulate() + } + + window.SwapBuffers() + } +} + +func updateScaleMat(x, y float32) { + scaleLoc := gl.GetUniformLocation(programId, gl.Str("scale\x00")) + scaleMat := mgl32.Scale3D(x, y, 1) + gl.UniformMatrix4fv(scaleLoc, 1, false, &scaleMat[0]) +} + +func updateCameraLocMat() { + cameraLocLoc := gl.GetUniformLocation(programId, gl.Str("cameraLoc\x00")) + cameraLocMat := mgl32.Translate3D(cameraX, -cameraY, 0) + gl.UniformMatrix4fv(cameraLocLoc, 1, false, &cameraLocMat[0]) +} + +func updateOverlayTex() { + width, height := simulator.Size() + // update PBO + gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, overlayPBO) + overlayPBOPtr := gl.MapBuffer(gl.PIXEL_UNPACK_BUFFER, gl.READ_WRITE) + if overlayPBOPtr != nil { + overlayPBOSlice := (*[1 << 30]byte)(overlayPBOPtr)[:width*height*4 : width*height*4] + + simulator.PerPixel(func(x, y int, state bool) { + index := x + y*width + var value byte + if state { + value = 255 + } else { + value = 0 + } + overlayPBOSlice[index*4] = value + overlayPBOSlice[index*4+1] = value + overlayPBOSlice[index*4+2] = value + overlayPBOSlice[index*4+3] = 255 + }) + + success := gl.UnmapBuffer(gl.PIXEL_UNPACK_BUFFER) + if !success { + log.Println("There was a problem unmapping pbo.") + } + } + gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, 0) + + // unpack PBO to overlay texture + gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, overlayPBO) + gl.BindTexture(gl.TEXTURE_2D, overlayTex) + gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 512, 512, gl.RGBA, gl.UNSIGNED_BYTE, gl.PtrOffset(0)) + gl.BindTexture(gl.TEXTURE_2D, 0) + gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, 0) +} + +func loadTexture(img image.Image) (uint32, error) { + texId := uint32(0) + gl.GenTextures(1, &texId) + gl.BindTexture(gl.TEXTURE_2D, texId) + + rgba := image.NewRGBA(img.Bounds()) + + if rgba.Stride != rgba.Rect.Size().X*4 { + return 0, errors.New("Unsupported stride.") + } + draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src) + + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(img.Bounds().Dx()), int32(img.Bounds().Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix)) + + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + + return texId, nil +} + +func loadShader(rawSource string, shaderType uint32) (uint32, error) { + rawSource += "\x00" + shader := gl.CreateShader(shaderType) + + source, free := gl.Strs(rawSource) + gl.ShaderSource(shader, 1, source, nil) + free() + gl.CompileShader(shader) + + var status int32 + gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) + if status == gl.FALSE { + var logLength int32 + gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength) + + log := strings.Repeat("\x00", int(logLength+1)) + gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log)) + + return 0, errors.New("Failed to compile shader : " + log) + } + + return shader, nil +} + +func setProjectionMat(shaderProgram uint32, width, ratio float32) { + height := width * ratio + hw := width / 2.0 + hh := height / 2.0 + projectionLoc := gl.GetUniformLocation(programId, gl.Str("projection\x00")) + projectionMat := mgl32.Ortho2D(-hw, hw, -hh, hh) + gl.ProgramUniformMatrix4fv(shaderProgram, projectionLoc, 1, false, &(projectionMat[0])) + fmt.Printf("Set size : %f, %f\n", width, height) +} + +func sizeCallback(w *glfw.Window, width, height int) { + gl.Viewport(0, 0, int32(width), int32(height)) + setProjectionMat(programId, float32(width), float32(height)/float32(width)) +} + +func scrollCallback(w *glfw.Window, xoff, yoff float64) { + cameraZoom += float32(yoff / 10.0) + updateScaleMat(512*cameraZoom, 512*cameraZoom) +} + +func mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { + if button == glfw.MouseButtonMiddle { + switch action { + case glfw.Press: + MMB = true + prevCursorXPos, prevCursorYPos = w.GetCursorPos() + case glfw.Release: + MMB = false + } + } else if button == glfw.MouseButtonLeft { + x, y := w.GetCursorPos() + width, height := w.GetSize() + + oriX := (x-float64(width)/2)/float64(512*cameraZoom) - float64(cameraX) + oriY := (y-float64(height)/2)/float64(512*cameraZoom) - float64(cameraY) + + xIdx := int((oriX + 0.5) * 512) + yIdx := int((oriY + 0.5) * 512) + + if action == glfw.Press { + simulator.Set(xIdx, yIdx, true) + } else if action == glfw.Release { + simulator.Set(xIdx, yIdx, false) + } + } +} + +func cursorPosCallback(w *glfw.Window, xpos, ypos float64) { + if MMB { + dx := xpos - prevCursorXPos + dy := ypos - prevCursorYPos + + cameraX += float32(dx) / (float32(512) * cameraZoom) + cameraY += float32(dy) / (float32(512) * cameraZoom) + + prevCursorXPos = xpos + prevCursorYPos = ypos + + updateCameraLocMat() + } +} diff --git a/gate.go b/gate.go new file mode 100644 index 0000000..f87bd44 --- /dev/null +++ b/gate.go @@ -0,0 +1,54 @@ +package gobls + +import ( + "math/rand" +) + +type point struct { + x, y int +} + +type gate struct { + state bool + slowState float32 + + in, out point + inIdx, outIdx int + inGates []int +} + +func (g *gate) setState(newState bool) { + g.state = newState + + if newState { + g.slowState = 1 + } else { + g.slowState = 0 + } +} + +func (g *gate) updateState(newState bool) { + if newState { + if g.state && g.slowState >= 1 { + return + } + + g.slowState += TIME_RAISE * rand.Float32() + + if g.slowState >= 1 { + g.slowState = 1 + g.state = true + } + } else { + if !g.state && g.slowState <= 0 { + return + } + + g.slowState -= TIME_FALL * rand.Float32() + + if g.slowState <= 0 { + g.slowState = 0 + g.state = false + } + } +} diff --git a/simulator.go b/simulator.go new file mode 100644 index 0000000..1cecbc1 --- /dev/null +++ b/simulator.go @@ -0,0 +1,340 @@ +package gobls + +import ( + "image" + "image/color" + "image/png" + "math/rand" + "os" + "time" +) + +const ( + BRIGHT_MIN = 223 + + TIME_RAISE = 0.5 + TIME_FALL = 0.5 + TIME_RANDOM = 0.5 +) + +type Simulator struct { + prevImage image.Image + curImage image.Image + + width int + height int + + wireMap [][]int + wireRemap []int + + states []bool // wire states + + gates []*gate // not gates + gatePerm []int // permutation for not gates +} + +func NewSimulator() *Simulator { + simulator := new(Simulator) + + return simulator +} + +func (simulator *Simulator) LoadImage(img image.Image) { + simulator.prevImage = simulator.curImage + simulator.curImage = img + + width := img.Bounds().Max.X + height := img.Bounds().Max.Y + + // search wires horizontally + wireMap := make([][]int, height) + for i := range wireMap { + wireMap[i] = make([]int, width) + } + + wireIdx := -1 + for y := 0; y < height; y++ { + pixel := img.At(0, y) + + if isConductive(pixel) { + wireIdx++ + + wireMap[y][0] = wireIdx + } else { + wireMap[y][0] = -1 + } + + for x := 1; x < width; x++ { + prevPixel := img.At(x-1, y) + curPixel := img.At(x, y) + + if isConductive(curPixel) { + if !isConductive(prevPixel) { + wireIdx++ + } + + wireMap[y][x] = wireIdx + } else { + wireMap[y][x] = -1 + } + } + } + + // remap wires + wireRemap := make([]int, wireIdx+1) + for i := range wireRemap { + wireRemap[i] = i + } + + for y := 1; y < height; y++ { + for x := 0; x < width; x++ { + upperWire := wireMap[y-1][x] + lowerWire := wireMap[y][x] + + if upperWire >= 0 && lowerWire >= 0 { + upperIdx := wireRemap[upperWire] + lowerIdx := wireRemap[lowerWire] + if upperIdx != lowerIdx { + // connect two wire + for i, v := range wireRemap { + if v == lowerIdx { + wireRemap[i] = upperIdx + } + } + } + } + } + } + + // search crossing wires and not gates + gates := make([]*gate, 0) + for y := 1; y < height-1; y++ { + for x := 1; x < width-1; x++ { + if wireMap[y][x] < 0 && wireMap[y][x-1] >= 0 && wireMap[y][x+1] >= 0 && wireMap[y-1][x] >= 0 && wireMap[y+1][x] >= 0 { + flag := 0 + + if wireMap[y-1][x-1] >= 0 { + flag |= 1 << 0 + } + if wireMap[y-1][x+1] >= 0 { + flag |= 1 << 1 + } + if wireMap[y+1][x+1] >= 0 { + flag |= 1 << 2 + } + if wireMap[y+1][x-1] >= 0 { + flag |= 1 << 3 + } + + switch flag { + case 0: // crossing wire + // connect up, down wire and left, right wire + upperIdx := wireRemap[wireMap[y-1][x]] + lowerIdx := wireRemap[wireMap[y+1][x]] + leftIdx := wireRemap[wireMap[y][x-1]] + rightIdx := wireRemap[wireMap[y][x+1]] + + for i, v := range wireRemap { + if v == lowerIdx { + wireRemap[i] = upperIdx + } + } + for i, v := range wireRemap { + if v == rightIdx { + wireRemap[i] = leftIdx + } + } + case 1 + 2: // not gate down + gates = append(gates, &gate{in: point{x, y - 1}, out: point{x, y + 1}}) + case 2 + 4: // not gate left + gates = append(gates, &gate{in: point{x + 1, y}, out: point{x - 1, y}}) + case 4 + 8: // not gate up + gates = append(gates, &gate{in: point{x, y + 1}, out: point{x, y - 1}}) + case 8 + 1: // not gate right + gates = append(gates, &gate{in: point{x - 1, y}, out: point{x + 1, y}}) + } + } + } + } + + // resolve gate in, out idx + for _, gate := range gates { + gate.inIdx = wireRemap[wireMap[gate.in.y][gate.in.x]] + gate.outIdx = wireRemap[wireMap[gate.out.y][gate.out.x]] + } + + // find input gates + for _, gate := range gates { + gate.inGates = make([]int, 0) + for gateIdx, inputGate := range gates { + if gate.inIdx == inputGate.outIdx { + gate.inGates = append(gate.inGates, gateIdx) + } + } + } + + // gate permutation + gatePerm := rand.Perm(len(gates)) + + // init wire state + states := make([]bool, len(wireRemap)) + for i := range states { + states[i] = false + } + + // load previous states + if simulator.prevImage != nil { + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + if wireMap[y][x] != 0 && isConductive(simulator.prevImage.At(x, y)) { + states[wireRemap[wireMap[y][x]]] = true + } + } + } + } + + simulator.width = width + simulator.height = height + simulator.wireMap = wireMap + simulator.wireRemap = wireRemap + simulator.gates = gates + simulator.states = states + simulator.gatePerm = gatePerm + + simulator.Simulate() + + // ------------------------------------------------------------TEST BEGIN + // wire remapping test image + wireRemapImgFile, err := os.Create("wireMap.png") + if err != nil { + panic(err) + } + wireRemapImg := image.NewRGBA(img.Bounds()) + + rand.Seed(time.Now().UTC().UnixNano()) + randomRColor := rand.Perm(200) + randomGColor := rand.Perm(200) + randomBColor := rand.Perm(200) + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + wire := wireMap[y][x] + if wire >= 0 { + idx := wireRemap[wire] + overlay := 0 + if states[idx] { + overlay = 128 + } + + wireRemapImg.Set(x, y, color.RGBA{ + uint8(randomRColor[idx%200] + 56 - overlay), + uint8(randomGColor[idx%200] + 56 - overlay), + uint8(randomBColor[idx%200] + 56 - overlay), + 255, + }) + } else { + wireRemapImg.Set(x, y, color.RGBA{0, 0, 0, 255}) + } + } + } + + png.Encode(wireRemapImgFile, wireRemapImg) + + // gates test image + gateImgFile, err := os.Create("gate.png") + if err != nil { + panic(err) + } + gateImg := image.NewRGBA(img.Bounds()) + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + gateImg.Set(x, y, color.RGBA{0, 0, 0, 0}) + } + } + for _, gate := range gates { + gateImg.Set(gate.in.x, gate.in.y, color.RGBA{255, 0, 0, 255}) + gateImg.Set(gate.out.x, gate.out.y, color.RGBA{0, 255, 0, 255}) + } + + png.Encode(gateImgFile, gateImg) + // ---------------------------------------------------------TEST END +} + +func (simulator *Simulator) Simulate() { + for i := range simulator.gates { + g := simulator.gates[simulator.gatePerm[i]] + newState := !simulator.gateInput(g) + g.updateState(newState) + } + + simulator.storeGateStatesToWires() +} + +func (simulator *Simulator) Set(x, y int, state bool) bool { + wire := simulator.wireMap[y][x] + + if wire >= 0 { + wireIdx := simulator.wireRemap[wire] + simulator.states[wireIdx] = state + + return true + } + + return false +} + +func (simulator *Simulator) Size() (int, int) { + return simulator.width, simulator.height +} + +func (simulator *Simulator) PerPixel(f func(int, int, bool)) { + for y := 0; y < simulator.height; y++ { + for x := 0; x < simulator.width; x++ { + wire := simulator.wireMap[y][x] + state := false + if wire != -1 { + state = simulator.states[simulator.wireRemap[wire]] + } + f(x, y, state) + } + } +} + +func isConductive(pixel color.Color) bool { + r, g, b, _ := pixel.RGBA() + + return r >= BRIGHT_MIN || g >= BRIGHT_MIN || b >= BRIGHT_MIN +} + +func (simulator *Simulator) gateInput(g *gate) bool { + if g.inGates == nil || len(g.inGates) == 0 { + return simulator.states[g.inIdx] + } else { + for _, inGate := range g.inGates { + if simulator.gates[inGate].state { + return true + } + } + + return false + } +} + +func (simulator *Simulator) loadGateStatesFromWire() { + for _, g := range simulator.gates { + g.setState(simulator.states[g.outIdx]) + } +} + +func (simulator *Simulator) storeGateStatesToWires() { + for _, g := range simulator.gates { + simulator.states[g.outIdx] = false + } + + for _, g := range simulator.gates { + if g.state { + simulator.states[g.outIdx] = true + } + } +} diff --git a/simulator_test.go b/simulator_test.go new file mode 100644 index 0000000..f25a975 --- /dev/null +++ b/simulator_test.go @@ -0,0 +1,26 @@ +package gobls_test + +import ( + "github.com/rlj1202/go-BitmapLogicSimulator" + "image" + _ "image/png" + "os" + "testing" +) + +func TestLoad(t *testing.T) { + imgFile, err := os.Open("test.png") + if err != nil { + t.Error(err) + return + } + + img, _, err := image.Decode(imgFile) + if err != nil { + panic(err) + } + + simulator := gobls.NewSimulator() + simulator.LoadImage(img) + simulator.Simulate() +}