// Package main stl.go - GOOS=js GOARCH=wasm go build -ldflags="-X main.stlFileName=STLFILE.stl" stl.go
package main
import (
"bytes"
"encoding/base64"
"html/template"
"math"
"crypto/rand"
"reflect"
"runtime"
"strings"
"strconv"
"syscall/js"
"time"
"unsafe"
"sync"
"github.com/go-gl/mathgl/mgl32"
"gitlab.com/russoj88/stl/stl"
)
var wg sync.WaitGroup
var running = true
var gl js.Value
func generateSphereVertices(radius float32, stacks, slices int) ([]float32, []uint32) {
var vertices []float32
var indices []uint32
for i := 0; i <= stacks; i++ {
phi := float32(i) * float32(math.Pi) / float32(stacks)
for j := 0; j <= slices; j++ {
theta := float32(j) * 2.0 * float32(math.Pi) / float32(slices)
x := radius * float32(math.Sin(float64(phi))) * float32(math.Cos(float64(theta)))
y := radius * float32(math.Sin(float64(phi))) * float32(math.Sin(float64(theta)))
z := radius * float32(math.Cos(float64(phi)))
vertices = append(vertices, x, y, z)
}
}
for i := 0; i < stacks; i++ {
for j := 0; j <= slices; j++ {
indices = append(indices, uint32(i*(slices+1)+j), uint32((i+1)*(slices+1)+j))
}
}
return vertices, indices
}
func prependChild(newElement, parent js.Value) {
firstChild := parent.Get("firstChild")
if firstChild.IsNull() {
parent.Call("appendChild", newElement)
} else {
parent.Call("insertBefore", newElement, firstChild)
}
}
var verticesNative = []float32{
-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1,
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1,
-1, -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1,
1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1,
-1, -1, -1, -1, -1, 1, 1, -1, 1, 1, -1, -1,
-1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1,
}
var colorsNative = []float32{
5, 3, 7, 5, 3, 7, 5, 3, 7, 5, 3, 7,
1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 3,
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
}
var indicesNative = []uint32{
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7,
8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15,
16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23,
}
const vertShaderCode = `
attribute vec3 position;
uniform mat4 Pmatrix;
uniform mat4 Vmatrix;
uniform mat4 Mmatrix;
attribute vec3 color;
varying vec3 vColor;
void main(void) {
gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);
vColor = color;
}
`
const fragShaderCode = `
precision mediump float;
varying vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor, 1.);
}
`
const fragShaderCode1 = `
precision mediump float;
uniform vec3 uBaseColor; // Color value at the base
uniform vec3 uTopColor; // Color value at the top
varying vec3 vPosition; // Interpolated vertex position
void main(void) {
float t = (vPosition.y + 1.0) * 0.5; // Normalize the y-coordinate to [0, 1]
vec3 rainbowColor = mix(uBaseColor, uTopColor, t);
gl_FragColor = vec4(rainbowColor, 1.0);
}
`
const vertShaderCode1 = `
attribute vec3 position;
uniform mat4 Pmatrix;
uniform mat4 Vmatrix;
uniform mat4 Mmatrix;
varying vec3 vPosition; // Pass vertex position to fragment shader
void main(void) {
gl_Position = Pmatrix * Vmatrix * Mmatrix * vec4(position, 1.0);
vPosition = position; // Pass vertex position to fragment shader
}
`
var (
// ht bool
done chan struct{}
stlFileName string
originalHTML string
render Renderer
existingFooter js.Value
body js.Value
footer js.Value
speedSliderXValue js.Value
speedSliderYValue js.Value
speedSliderZValue js.Value
speedSliderZoomValue js.Value
canvasElement js.Value
currentZoom float32 = 3
)
func parseBase64File(input string) (output []byte, err js.Value) {
searchString := "base64,"
index := strings.Index(input, searchString)
if index < 0 {
err = js.Global().Get("Error").New("Error opening file")
return
}
sBuffer := input[index+len(searchString):]
output, decodeErr := base64.StdEncoding.DecodeString(sBuffer)
if decodeErr != nil {
err = js.Global().Get("Error").New(decodeErr.Error())
return
}
return output, js.Null()
}
func uploaded(_ js.Value, args []js.Value) interface{} { // nolint
js.Global().Call("alert", "Finished uploading")
result := args[0].Get("target").Get("result").String()
func() {
defer func() {
if r := recover(); r != nil {
// fmt.Println("Recovered in upload", r)
js.Global().Call("alert", "Failed to parse file")
}
}()
uploadedFile, err := parseBase64File(result)
if !err.IsUndefined() && !err.IsNull() {
js.Global().Call("alert", err.Get("Error"))
}
stlSolid, err1 := NewSTL(uploadedFile)
if err1 != nil {
js.Global().Call("alert", "Could not parse file")
}
vert, colors, indices := stlSolid.GetModel()
modelSize := getMaxScalar(vert)
currentZoom = modelSize * 3
render.SetZoom(currentZoom)
render.SetModel(colors, vert, indices)
}()
return nil
}
func getMaxScalar(vertices []float32) float32 {
var max float32
for baseIndex := 0; baseIndex < len(vertices); baseIndex += 3 {
testScale := scalar(vertices[baseIndex], vertices[baseIndex], vertices[baseIndex])
if testScale > max {
max = testScale
}
}
return max
}
func scalar(x float32, y float32, z float32) float32 {
xy := math.Sqrt(float64(x*x + y*y))
return float32(math.Sqrt(xy*xy + float64(z*z)))
}
func cryptoRandFloat32() float32 {
b := make([]byte, 4)
_, err := rand.Read(b)
if err != nil {
panic("crypto/rand read failed: " + err.Error())
}
u := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
return float32(u) / float32(math.MaxUint32)
}
func main() {
//stlFileName = "TO-247.stl"
time.Sleep(time.Second)
if strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "mobile") {
return
}
tdata := struct {
XRange, YRange, ZRange, ZMin, ZMax float64
XStep, YStep, ZStep, ZoomStep float64
}{
XRange: 1, YRange: 1, ZRange: 1, ZMin: 0, ZMax: 50,
XStep: 0.01, YStep: 0.01, ZStep: 0.01, ZoomStep: 0.1,
}
if stlFileName != ".stl" && stlFileName != "" {
tdata.ZMin = 10
tdata.ZMax = 1000
}
tmpl, _ := template.New("main").Parse(controlsHTML)
var buf bytes.Buffer
_ = tmpl.Execute(&buf, tdata)
//<canvas id='gocanvas'></canvas>
doc := js.Global().Get("document")
body = doc.Get("body")
// body.Set("innerHTML", rawHTML)
existingFooter = doc.Call("getElementsByTagName", "footer").Index(0)
if existingFooter.Truthy() {
originalHTML = existingFooter.Get("innerHTML").String()
footer = doc.Call("createElement", "footer")
footer.Set("innerHTML", originalHTML+buf.String())
body.Call("replaceChild", footer, existingFooter)
} else {
footer = doc.Call("createElement", "footer")
footer.Set("innerHTML", buf.String())
body.Call("appendChild", footer)
}
doc = js.Global().Get("document")
// canvasResizeCallback := js.FuncOf(canvasResize)
canvasElement = doc.Call("getElementById", "gocanvas")
// js.Global().Get("window").Call("addEventListener", "resize", canvasResizeCallback)
width := doc.Get("body").Get("clientWidth").Int()
height := doc.Get("body").Get("clientHeight").Int()
canvasElement.Set("width", width)
canvasElement.Set("height", height)
sliderSpeedXCallback := js.FuncOf(sliderChangeX)
speedSliderX := doc.Call("getElementById", "speedSliderX")
speedSliderX.Call("addEventListener", "input", sliderSpeedXCallback)
speedSliderXValue = doc.Call("getElementById", "speedSliderXValue")
sliderSpeedYCallback := js.FuncOf(sliderChangeY)
speedSliderY := doc.Call("getElementById", "speedSliderY")
speedSliderY.Call("addEventListener", "input", sliderSpeedYCallback)
speedSliderYValue = doc.Call("getElementById", "speedSliderYValue")
sliderSpeedZCallback := js.FuncOf(sliderChangeZ)
speedSliderZ := doc.Call("getElementById", "speedSliderZ")
speedSliderZ.Call("addEventListener", "input", sliderSpeedZCallback)
speedSliderZValue = doc.Call("getElementById", "speedSliderZValue")
sliderSpeedZoomCallback := js.FuncOf(sliderChangeZoom)
speedSliderZoom := doc.Call("getElementById", "speedSliderZoom")
speedSliderZoom.Call("addEventListener", "input", sliderSpeedZoomCallback)
speedSliderZoomValue = doc.Call("getElementById", "speedSliderZoomValue")
//zoomChangeCallback := js.FuncOf(zoomChange)
//js.Global().Get("window").Call("addEventListener", "wheel", zoomChangeCallback)
stopButtonCallback := js.FuncOf(stopApplication)
stopButton := doc.Call("getElementById", "stop")
stopButton.Call("addEventListener", "click", stopButtonCallback)
defer stopButtonCallback.Release()
if stlFileName != ".stl" && stlFileName != "" {
response := js.Global().Call("fetch", "/stl/base64/"+stlFileName)
promise := response.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
if p[0].Get("ok").Bool() {
return p[0].Call("text")
}
return "Error fetching stereolithograph"
}))
promise.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
result := p[0].String()
uploadedFile, _ := parseBase64File(result) // nolint
// uploadedFile, err := parseBase64File(result)
// if err != nil {
// return nil
// js.Global().Call("alert", "Could not parse fetched file")
// }
stlSolid, _ := NewSTL(uploadedFile) // nolint
// stlSolid, err := NewSTL(uploadedFile)
// if err != nil {
// return nil
// js.Global().Call("alert", "Could not parse fetched file")
// }
vert, colors, indices := stlSolid.GetModel()
modelSize := getMaxScalar(vert)
currentZoom := modelSize * 3
render.SetZoom(currentZoom)
render.SetModel(colors, vert, indices)
return nil
}))
}
gl = canvasElement.Call("getContext", "webgl")
if gl.IsUndefined() {
gl = canvasElement.Call("getContext", "experimental-webgl")
}
if gl.IsUndefined() {
js.Global().Call("alert", "browser might not support webgl")
return
}
config := InitialConfig{
Width: width,
Height: height,
SpeedX: 0,
SpeedY: 0,
SpeedZ: 0,
Vertices: verticesNative,
Indices: indicesNative,
Colors: colorsNative,
FragmentShaderCode: fragShaderCode,
VertexShaderCode: vertShaderCode,
}
// config.SpeedX = rand.Float32()/20
// config.SpeedY = rand.Float32()/20
// config.SpeedZ = rand.Float32()/20
// ht = rand.Intn(2) != 0
// if ht {
config.SpeedX = cryptoRandFloat32() / 20
config.SpeedY = cryptoRandFloat32() / 20
config.SpeedZ = cryptoRandFloat32() / 20
config.Vertices, config.Indices = generateSphereVertices(float32(1.0),30,30)
if stlFileName == ".stl" || stlFileName == "" {
config.FragmentShaderCode, config.VertexShaderCode = fragShaderCode1, vertShaderCode1
}
// }
//configV, configI := generateSphereVertices(float32(1.0),30,30)
//config.Vertices, config.Indices = configV, []uint32(configI)
if stlFileName == ".stl" || stlFileName == "" {
ismobile := strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "mobile")
islinux := strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "linux")
if !ismobile && islinux && !strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "firefox") {
wg.Add(1)
logoimg := js.Global().Get("document").Call("getElementById", "logo")
if !logoimg.IsNull() {
logoimg.Call("remove")
}
var htmllogo string
htmllogo = "logolarge.html"
if ismobile {
htmllogo = "mobilelogo.html"
}
response := js.Global().Call("fetch", htmllogo)
promise := response.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
if p[0].Get("ok").Bool() {
return p[0].Call("text")
}
return "Error fetching ASCII art"
}))
promise.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
asciiArt := p[0].String()
doc := js.Global().Get("document")
htmlanimationdiv := doc.Call("getElementById", "htmlanimation")
var lines []string
if !ismobile && islinux {
lines = strings.Split(asciiArt, "\n")
}
div1 := doc.Call("createElement", "div")
div1.Get("style").Set("margin", "0")
div1.Get("style").Set("padding", "0")
div2 := doc.Call("createElement", "div")
div2.Get("style").Set("margin", "0")
div2.Get("style").Set("padding", "0")
htmlanimationdiv.Call("appendChild", div1)
htmlanimationdiv.Call("appendChild", div2)
if !ismobile && islinux {
index := 0
index1 := len(lines) - 1
var appendLineWithDelay func()
appendLineWithDelay = func() {
if index1 >= 0 && index < len(lines) && index < index1 {
pre1 := doc.Call("createElement", "pre")
pre1.Get("style").Set("font-size", "1px")
pre1.Get("style").Set("text-align", "center")
pre1.Get("style").Set("margin", "0")
pre1.Get("style").Set("padding", "0")
pre1.Set("innerHTML", lines[index]+"\n")
pre2 := doc.Call("createElement", "pre")
pre2.Get("style").Set("font-size", "1px")
pre2.Get("style").Set("text-align", "center")
pre2.Get("style").Set("margin", "0")
pre2.Get("style").Set("padding", "0")
pre2.Set("innerHTML", lines[index1]+"\n")
div1.Call("appendChild", pre1)
prependChild(pre2, div2)
index++
index1--
js.Global().Call("setTimeout", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
appendLineWithDelay()
return nil
}), 0)
} else {
wg.Done()
}
}
appendLineWithDelay()
} else {
htmlanimationdiv.Set("innerHTML", "<pre style='margin: 0; padding: 0; font-size: 1px;'>"+asciiArt+"</pre>")
}
return nil
}))
wg.Wait()
htmlanimationdiv := doc.Call("getElementById", "htmlanimation")
htmlanimationdiv.Get("style").Set("margin", "0")
htmlanimationdiv.Get("style").Set("position", "absolute")
htmlanimationdiv.Get("style").Set("top", "50%")
htmlanimationdiv.Get("style").Set("-ms-transform", "translateY(-50%)")
htmlanimationdiv.Get("style").Set("transform", "translate(50%, -50%)")
htmlanimationdiv.Get("style").Set("z-index", "-1")
}
time.Sleep(time.Second)
}
var jsErr js.Value
render, jsErr = NewRenderer(gl, config)
if !jsErr.IsNull() {
js.Global().Call("alert", "Cannot load webgl ")
return
}
render.SetZoom(currentZoom)
defer render.Release()
x, y, z := render.GetSpeed()
speedSliderX.Set("value", strconv.FormatFloat(float64(x), 'f', -1, 64))
if x > 0 {speedSliderXValue.Set("innerHTML", "+"+strconv.FormatFloat(float64(x), 'f', 2, 64))}
if x == 0 {speedSliderXValue.Set("innerHTML", " "+strconv.FormatFloat(float64(x), 'f', 2, 64))}
if x < 0 {speedSliderXValue.Set("innerHTML", strconv.FormatFloat(float64(x), 'f', 2, 64))}
speedSliderY.Set("value", strconv.FormatFloat(float64(y), 'f', -1, 64))
if y > 0 {speedSliderYValue.Set("innerHTML", "+"+strconv.FormatFloat(float64(y), 'f', 2, 64))}
if y == 0 {speedSliderYValue.Set("innerHTML", "0"+strconv.FormatFloat(float64(y), 'f', 2, 64))}
if y < 0 {speedSliderYValue.Set("innerHTML", strconv.FormatFloat(float64(y), 'f', 2, 64))}
speedSliderZ.Set("value", strconv.FormatFloat(float64(z), 'f', -1, 64))
if z > 0 {speedSliderZValue.Set("innerHTML", "+"+strconv.FormatFloat(float64(z), 'f', 2, 64))}
if z == 0 {speedSliderZValue.Set("innerHTML", "0"+strconv.FormatFloat(float64(z), 'f', 2, 64))}
if z < 0 {speedSliderZValue.Set("innerHTML", strconv.FormatFloat(float64(z), 'f', 2, 64))}
var renderFrame js.Func
renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
render.Render(this, args)
js.Global().Call("requestAnimationFrame", renderFrame)
return nil
})
js.Global().Call("requestAnimationFrame", renderFrame)
done := make(chan struct{})
<-done
}
func stopApplication(_ js.Value, _ []js.Value) interface{} {
running = false
speedSliderZoomValue.Set("innerHTML", float32(10000))
currentZoom = float32(10000)
render.SetZoom(float32(10000))
footer.Set("innerHTML", originalHTML)
js.Global().Call("setTimeout", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
close(done)
done <- struct{}{}
return nil
}), time.Duration(5*time.Second).Milliseconds())
return nil
}
func canvasResize(_ js.Value, _ []js.Value) interface{} { // nolint
width := canvasElement.Get("clientWidth").Int()
height := canvasElement.Get("clientHeight").Int()
canvasElement.Set("width", width)
canvasElement.Set("height", height)
render.SetSize(height*2, width)
return nil
}
func sliderChangeX(this js.Value, _ []js.Value) interface{} {
var speed float64
sSpeed := this.Get("value").String()
speed, _ = strconv.ParseFloat(sSpeed, 64)
render.SetSpeedX(float32(speed))
if speed > 0 { speedSliderXValue.Set("innerHTML", "+"+strconv.FormatFloat(speed, 'f', 2, 32)) }
if speed == 0 { speedSliderXValue.Set("innerHTML", "0"+strconv.FormatFloat(speed, 'f', 2, 32)) }
if speed < 0 { speedSliderXValue.Set("innerHTML", strconv.FormatFloat(speed, 'f', 2, 32)) }
return nil
}
func sliderChangeY(this js.Value, _ []js.Value) interface{} {
var speed float64
sSpeed := this.Get("value").String()
speed, _ = strconv.ParseFloat(sSpeed, 64)
render.SetSpeedY(float32(speed))
if speed > 0 { speedSliderYValue.Set("innerHTML", "+"+strconv.FormatFloat(speed, 'f', 2, 32)) }
if speed == 0 { speedSliderYValue.Set("innerHTML", "0"+strconv.FormatFloat(speed, 'f', 2, 32)) }
if speed < 0 { speedSliderYValue.Set("innerHTML", strconv.FormatFloat(speed, 'f', 2, 32)) }
return nil
}
func sliderChangeZ(this js.Value, _ []js.Value) interface{} {
var speed float64
sSpeed := this.Get("value").String()
speed, _ = strconv.ParseFloat(sSpeed, 64)
render.SetSpeedZ(float32(speed))
if speed > 0 { speedSliderZValue.Set("innerHTML", "+"+strconv.FormatFloat(speed, 'f', 2, 32)) }
if speed == 0 { speedSliderZValue.Set("innerHTML", "0"+strconv.FormatFloat(speed, 'f', 2, 32)) }
if speed < 0 { speedSliderZValue.Set("innerHTML", strconv.FormatFloat(speed, 'f', 2, 32)) }
return nil
}
func sliderChangeZoom(this js.Value, _ []js.Value) interface{} {
sSpeed := this.Get("value").String()
speed, _ := strconv.ParseFloat(sSpeed, 64)
if speed < 10 { speedSliderZoomValue.Set("innerHTML", "000"+strconv.FormatFloat(speed, 'f', 2, 32)) } else if speed < 100 { speedSliderZoomValue.Set("innerHTML", "00"+strconv.FormatFloat(speed, 'f', 2, 32)) } else if speed < 1000 { speedSliderZoomValue.Set("innerHTML", "0"+strconv.FormatFloat(speed, 'f', 2, 32)) } else { speedSliderZoomValue.Set("innerHTML", strconv.FormatFloat(speed, 'f', 2, 32)) }
currentZoom = float32(speed)
render.SetZoom(currentZoom)
return nil
}
func zoomChange(_ js.Value, args []js.Value) interface{} {
deltaY := args[0].Get("deltaY").Float()
deltaScale := 1 - (float32(deltaY) * 0.001)
currentZoom *= deltaScale
render.SetZoom(currentZoom)
speedSliderZoomValue.Set("innerHTML", strconv.FormatFloat(float64(currentZoom), 'f', 2, 32))
return nil
}
// Model is an interface for a model
type Model interface {
GetModel() ([]float32, []float32, []uint16)
}
func cryptoRandIntn(max int) (int, js.Value) {
if max <= 0 { return 0, js.Global().Get("Error").New("max must be a positive integer") }
numBytes := (max + 7) / 8
maxBytes := 1 << (numBytes * 8)
randBytes := make([]byte, numBytes)
randNum := 0
for {
_, err := rand.Read(randBytes)
if err != nil { return 0, js.Global().Get("Error").New("error generating random number") }
for _, b := range randBytes { randNum = (randNum << 8) | int(b) }
if randNum < maxBytes-maxBytes%max { break }
}
return randNum % max, js.Null()
}
// NewSTL returns a new STL & errror
func NewSTL(buffer []byte) (output STL, err error) {
bufferReader := bytes.NewReader(buffer)
solid, err := stl.From(bufferReader)
if err != nil {
return
}
// fmt.Printf("Parsed in %d Triangles\n", solid.TriangleCount)
numColors, _ := cryptoRandIntn(5)
numColors += 2 // Generates a random number between 0 and 4
colors := GenerateGradient(numColors, int(solid.TriangleCount))
var index uint32
for i, triangle := range solid.Triangles {
colorR := colors[i].Red
colorG := colors[i].Green
colorB := colors[i].Blue
output.vertices = append(output.vertices, triangle.Vertices[0].X)
output.vertices = append(output.vertices, triangle.Vertices[0].Y)
output.vertices = append(output.vertices, triangle.Vertices[0].Z)
output.indices = append(output.indices, index)
output.colors = append(output.colors, colorR)
output.colors = append(output.colors, colorG)
output.colors = append(output.colors, colorB)
index++
output.vertices = append(output.vertices, triangle.Vertices[1].X)
output.vertices = append(output.vertices, triangle.Vertices[1].Y)
output.vertices = append(output.vertices, triangle.Vertices[1].Z)
output.indices = append(output.indices, index)
output.colors = append(output.colors, colorR)
output.colors = append(output.colors, colorG)
output.colors = append(output.colors, colorB)
index++
output.vertices = append(output.vertices, triangle.Vertices[2].X)
output.vertices = append(output.vertices, triangle.Vertices[2].Y)
output.vertices = append(output.vertices, triangle.Vertices[2].Z)
output.indices = append(output.indices, index)
output.colors = append(output.colors, colorR)
output.colors = append(output.colors, colorG)
output.colors = append(output.colors, colorB)
index++
}
return output, err
}
// STL is a stereolithograph
type STL struct {
vertices []float32
colors []float32
indices []uint32
}
// GetModel gets the model
func (s STL) GetModel() ([]float32, []float32, []uint32) {
return s.vertices, s.colors, s.indices
}
// InitialConfig is the initial config
type InitialConfig struct {
Width int
Height int
SpeedX float32
SpeedY float32
SpeedZ float32
Colors []float32
Vertices []float32
Indices []uint32
FragmentShaderCode string
VertexShaderCode string
}
// Renderer is the renderer
type Renderer struct {
glContext js.Value
glTypes GLTypes
colors js.Value
vertices js.Value
indices js.Value
colorBuffer js.Value
vertexBuffer js.Value
indexBuffer js.Value
numIndices int
numVertices int
fragShader js.Value
vertShader js.Value
shaderProgram js.Value
tmark float32
rotationX float32
rotationY float32
rotationZ float32
movMatrix mgl32.Mat4
PositionMatrix js.Value
ViewMatrix js.Value
ModelMatrix js.Value
height int
width int
speedX float32
speedY float32
speedZ float32
}
// NewRenderer returns a new renderer & error
func NewRenderer(gl js.Value, config InitialConfig) (r Renderer, err js.Value) {
// Get some WebGL bindings
r.glContext = gl
err = r.glTypes.New(r.glContext)
r.numIndices = len(config.Indices)
r.numVertices = len(config.Vertices)
r.movMatrix = mgl32.Ident4()
r.width = config.Width
r.height = config.Height
r.speedX = config.SpeedX
r.speedY = config.SpeedY
r.speedZ = config.SpeedZ
// Convert buffers to JS TypedArrays
r.UpdateColorBuffer(config.Colors)
r.UpdateVerticesBuffer(config.Vertices)
r.UpdateIndicesBuffer(config.Indices)
r.UpdateFragmentShader(config.FragmentShaderCode)
r.UpdateVertexShader(config.VertexShaderCode)
r.updateShaderProgram()
r.attachShaderProgram()
r.setContextFlags()
r.createMatrixes()
r.EnableObject()
return
}
// SetModel sets a new model
func (r *Renderer) SetModel(Colors []float32, Vertices []float32, Indices []uint32) {
// fmt.Println("Renderer.SetModel")
r.numIndices = len(Indices)
// fmt.Println("Number of Indices:", len(Indices))
r.UpdateColorBuffer(Colors)
// fmt.Println("Number of Colors:", len(Colors))
r.UpdateVerticesBuffer(Vertices)
// fmt.Println("Number of Vertices:", len(Vertices))
r.UpdateIndicesBuffer(Indices)
r.EnableObject()
}
// Release releases the renderer
func (r *Renderer) Release() {
return
// fmt.Println("Renderer.Release")
}
// EnableObject enables the object
func (r *Renderer) EnableObject() {
// fmt.Println("Renderer.EnableObject")
r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer)
}
// SetSpeedX set rotation x axis speed
func (r *Renderer) SetSpeedX(x float32) {
r.speedX = x
}
// SetSpeedY set rotation y axis speed
func (r *Renderer) SetSpeedY(y float32) {
r.speedY = y
}
// SetSpeedZ set rotation z axis speed
func (r *Renderer) SetSpeedZ(z float32) {
r.speedZ = z
}
// GetSpeed returns the rotation speeds
func (r *Renderer) GetSpeed() (x, y, z float32) {
return r.speedX, r.speedY, r.speedZ
}
// SetSize sets the size of the rendering
func (r *Renderer) SetSize(height, width int) {
r.height = height
r.width = width
// fmt.Println("Size", r.width, r.height)
}
func (r *Renderer) createMatrixes() {
ratio := float32(r.width) / float32(r.height)
// fmt.Println("Renderer.createMatrixes")
projMatrix := mgl32.Perspective(mgl32.DegToRad(45.0), ratio, 1, 100000.0)
projMatrixBuffer := (*[16]float32)(unsafe.Pointer(&projMatrix)) // nolint
typedProjMatrixBuffer := SliceToTypedArray([]float32((*projMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.PositionMatrix, false, typedProjMatrixBuffer)
viewMatrix := mgl32.LookAtV(mgl32.Vec3{3.0, 3.0, 3.0}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0})
viewMatrixBuffer := (*[16]float32)(unsafe.Pointer(&viewMatrix)) // nolint
typedViewMatrixBuffer := SliceToTypedArray([]float32((*viewMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer)
}
func (r *Renderer) setContextFlags() {
// fmt.Println("Renderer.setContextFlags")
r.glContext.Call("clearColor", 0.0, 0.0, 0.0, 0.0) // Color the screen is cleared to
// r.glContext.Call("clearDepth", 1.0) // Z value that is set to the Depth buffer every frame
r.glContext.Call("viewport", 0, 0, r.width, r.height) // Viewport size
r.glContext.Call("depthFunc", r.glTypes.LEqual)
}
// UpdateFragmentShader Updates the Fragment Shader
func (r *Renderer) UpdateFragmentShader(shaderCode string) {
// fmt.Println("Renderer.UpdateFragmentShader")
r.fragShader = r.glContext.Call("createShader", r.glTypes.FragmentShader)
r.glContext.Call("shaderSource", r.fragShader, shaderCode)
r.glContext.Call("compileShader", r.fragShader)
}
// UpdateVertexShader updates the vertex shader
func (r *Renderer) UpdateVertexShader(shaderCode string) {
// fmt.Println("Renderer.UpdateVertexShader")
r.vertShader = r.glContext.Call("createShader", r.glTypes.VertexShader)
r.glContext.Call("shaderSource", r.vertShader, shaderCode)
r.glContext.Call("compileShader", r.vertShader)
}
func (r *Renderer) updateShaderProgram() {
// fmt.Println("Renderer.updateShaderProgram")
if r.fragShader.IsUndefined() || r.vertShader.IsUndefined() {
return
}
r.shaderProgram = r.glContext.Call("createProgram")
r.glContext.Call("attachShader", r.shaderProgram, r.vertShader)
r.glContext.Call("attachShader", r.shaderProgram, r.fragShader)
r.glContext.Call("linkProgram", r.shaderProgram)
}
func (r *Renderer) attachShaderProgram() {
// fmt.Println("Renderer.attachShaderProgram")
r.PositionMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Pmatrix")
r.ViewMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Vmatrix")
r.ModelMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Mmatrix")
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer)
position := r.glContext.Call("getAttribLocation", r.shaderProgram, "position")
r.glContext.Call("vertexAttribPointer", position, 3, r.glTypes.Float, false, 0, 0)
r.glContext.Call("enableVertexAttribArray", position)
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer)
color := r.glContext.Call("getAttribLocation", r.shaderProgram, "color")
r.glContext.Call("vertexAttribPointer", color, 3, r.glTypes.Float, false, 0, 0)
r.glContext.Call("enableVertexAttribArray", color)
r.glContext.Call("useProgram", r.shaderProgram)
if stlFileName == ".stl" || stlFileName == "" {
uBaseColor := r.glContext.Call("getUniformLocation", r.shaderProgram, "uBaseColor")
uTopColor := r.glContext.Call("getUniformLocation", r.shaderProgram, "uTopColor")
uColor := r.glContext.Call("getUniformLocation", r.shaderProgram, "uColor")
r.glContext.Call("uniform3f", uBaseColor, 1.0, 0.0, 0.0)
r.glContext.Call("uniform3f", uTopColor, 0.0, 0.0, 1.0)
r.glContext.Call("uniform3f", uColor, 1.0, 1.0, 1.0)
}
}
// UpdateColorBuffer Updates the ColorBuffer
func (r *Renderer) UpdateColorBuffer(buffer []float32) {
// fmt.Println("Renderer.UpdateColorBuffer")
r.colors = SliceToTypedArray(buffer)
if r.colorBuffer.IsUndefined() {
r.colorBuffer = r.glContext.Call("createBuffer")
}
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer)
r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.colors, r.glTypes.StaticDraw)
}
// UpdateVerticesBuffer Updates the VerticesBuffer
func (r *Renderer) UpdateVerticesBuffer(buffer []float32) {
// fmt.Println("Renderer.UpdateVerticesBuffer")
r.vertices = SliceToTypedArray(buffer)
if r.vertexBuffer.IsUndefined() {
r.vertexBuffer = r.glContext.Call("createBuffer")
}
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer)
r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.vertices, r.glTypes.StaticDraw)
}
// UpdateIndicesBuffer Updates the IndicesBuffer
func (r *Renderer) UpdateIndicesBuffer(buffer []uint32) {
// fmt.Println("Renderer.UpdateIndicesBuffer")
r.indices = SliceToTypedArray(buffer)
if r.indexBuffer.IsUndefined() {
r.indexBuffer = r.glContext.Call("createBuffer")
}
r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer)
r.glContext.Call("bufferData", r.glTypes.ElementArrayBuffer, r.indices, r.glTypes.StaticDraw)
}
// Render renders
func (r *Renderer) Render(_ js.Value, args []js.Value) interface{} { // nolint
now := float32(args[0].Float())
tdiff := now - r.tmark
r.tmark = now
r.rotationX = r.rotationX + r.speedX*float32(tdiff)/500
r.rotationY = r.rotationY + r.speedY*float32(tdiff)/500
r.rotationZ = r.rotationZ + r.speedZ*float32(tdiff)/500
r.movMatrix = mgl32.HomogRotate3DX(r.rotationX)
r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DY(r.rotationY))
r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DZ(r.rotationZ))
modelMatrixBuffer := (*[16]float32)(unsafe.Pointer(&r.movMatrix)) // nolint
typedModelMatrixBuffer := SliceToTypedArray([]float32((*modelMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.ModelMatrix, false, typedModelMatrixBuffer)
r.glContext.Call("enable", r.glTypes.DepthTest)
r.glContext.Call("clear", r.glTypes.ColorBufferBit)
r.glContext.Call("clear", r.glTypes.DepthBufferBit)
usegltype := r.glTypes.Triangles
if stlFileName == ".stl" || stlFileName == "" {
// if ht {
usegltype = r.glTypes.Line
r.glContext.Call("drawArrays", r.glTypes.LineLoop, 0, r.numVertices/3)
// }
}
r.glContext.Call("drawElements", usegltype, r.numIndices, r.glTypes.UnsignedInt, 0)
return nil
}
// SetZoom Sets the Zoom
func (r *Renderer) SetZoom(currentZoom float32) {
// fmt.Println("Renderer.SetZoom")
viewMatrix := mgl32.LookAtV(mgl32.Vec3{currentZoom, currentZoom, currentZoom}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0})
viewMatrixBuffer := (*[16]float32)(unsafe.Pointer(&viewMatrix)) // nolint
typedViewMatrixBuffer := SliceToTypedArray([]float32((*viewMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer)
}
// NewColorInterpolation generates color interpolation
func NewColorInterpolation(a Color, b Color) ColorInterpolation {
return ColorInterpolation{
a,
b,
a.Subtract(b),
}
}
// ColorInterpolation is interpolated color
type ColorInterpolation struct {
startColor Color
endColor Color
deltaColor Color
}
// Interpolate interpolates
func (c ColorInterpolation) Interpolate(percent float32) Color {
scaled := c.deltaColor.MultiplyFloat(percent)
return c.startColor.Add(scaled)
}
// Color represents a color
type Color struct {
Red float32
Green float32
Blue float32
}
// NewRandomColor returns a New RandomColor
func NewRandomColor() Color {
const maxRGB = 255
var r, g, b float64
buf := make([]byte, 3)
rand.Read(buf)
r = float64(buf[0]) / 256
g = float64(buf[1]) / 256
b = float64(buf[2]) / 256
r = r * maxRGB
g = g * maxRGB
b = b * maxRGB
return Color{float32(r), float32(g), float32(b)}
}
// Subtract Subtracts color
func (c Color) Subtract(d Color) Color {
return Color{
c.Red - d.Red,
c.Green - d.Green,
c.Blue - d.Blue,
}
}
// Add Adds color
func (c Color) Add(d Color) Color {
return Color{
c.Red + d.Red,
c.Green + d.Green,
c.Blue + d.Blue,
}
}
// MultiplyFloat Multiplies Float
func (c Color) MultiplyFloat(x float32) Color {
return Color{
c.Red * x,
c.Green * x,
c.Blue * x,
}
}
// GenerateGradient Generates Gradient
func GenerateGradient(numColors int, steps int) []Color {
distribution := distributeColors(numColors, steps)
colors := make([]Color, numColors)
for i := 0; i < numColors; i++ {
colors[i] = NewRandomColor()
}
outputBuffer := make([]Color, 0, steps)
for index := 0; index < numColors; index++ {
if index >= numColors-1 {
size := steps - distribution[index]
interpolation := NewColorInterpolation(colors[index-1], colors[index])
buffer := generateSingleGradient(interpolation, size)
outputBuffer = append(outputBuffer, buffer...)
break
}
currentStep := distribution[index]
nextStep := distribution[index+1]
size := nextStep - currentStep
interpolation := NewColorInterpolation(colors[index], colors[index+1])
buffer := generateSingleGradient(interpolation, size)
outputBuffer = append(outputBuffer, buffer...)
}
return outputBuffer
}
func distributeColors(numColors int, steps int) []int {
diff := int(math.Ceil(float64(steps) / float64(numColors)))
output := make([]int, numColors)
for i := 0; i < numColors; i++ {
output[i] = diff * i
}
return output
}
func generateSingleGradient(c ColorInterpolation, numSteps int) []Color {
output := make([]Color, numSteps)
for i := 0; i < numSteps; i++ {
percent := float32(i) / float32(numSteps)
output[i] = c.Interpolate(percent)
}
return output
}
// GLTypes provides WebGL bindings.
type GLTypes struct {
StaticDraw js.Value
ArrayBuffer js.Value
ElementArrayBuffer js.Value
VertexShader js.Value
FragmentShader js.Value
Float js.Value
DepthTest js.Value
ColorBufferBit js.Value
DepthBufferBit js.Value
Triangles js.Value
UnsignedShort js.Value
UnsignedInt js.Value
LEqual js.Value
LineLoop js.Value
Line js.Value
}
// New grabs the WebGL bindings from a GL context.
func (types *GLTypes) New(gl js.Value) js.Value {
types.StaticDraw = gl.Get("STATIC_DRAW")
types.ArrayBuffer = gl.Get("ARRAY_BUFFER")
types.ElementArrayBuffer = gl.Get("ELEMENT_ARRAY_BUFFER")
types.VertexShader = gl.Get("VERTEX_SHADER")
types.FragmentShader = gl.Get("FRAGMENT_SHADER")
types.Float = gl.Get("FLOAT")
types.DepthTest = gl.Get("DEPTH_TEST")
types.ColorBufferBit = gl.Get("COLOR_BUFFER_BIT")
types.Triangles = gl.Get("TRIANGLES")
types.UnsignedShort = gl.Get("UNSIGNED_SHORT")
types.LEqual = gl.Get("LEQUAL")
types.DepthBufferBit = gl.Get("DEPTH_BUFFER_BIT")
types.LineLoop = gl.Get("LINE_LOOP")
types.Line = gl.Get("LINES")
enabled := gl.Call("getExtension", "OES_element_index_uint")
if !enabled.Truthy() {
return js.Global().Get("Error").New("missing extension: OES_element_index_uint")
}
types.UnsignedInt = gl.Get("UNSIGNED_INT")
return js.Null()
}
func sliceToByteSlice(s interface{}) []byte {
switch s := s.(type) {
case []int8:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []int16:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 2
h.Cap *= 2
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []int32:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 4
h.Cap *= 4
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []int64:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 8
h.Cap *= 8
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []uint8:
return s
case []uint16:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 2
h.Cap *= 2
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []uint32:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 4
h.Cap *= 4
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []uint64:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 8
h.Cap *= 8
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []float32:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 4
h.Cap *= 4
return *(*[]byte)(unsafe.Pointer(h)) // nolint
case []float64:
h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
h.Len *= 8
h.Cap *= 8
return *(*[]byte)(unsafe.Pointer(h)) // nolint
default:
panic("jsutil: unexpected value at sliceToBytesSlice: " + reflect.TypeOf(s).String())
}
}
// SliceToTypedArray converts Slice To TypedArray
func SliceToTypedArray(s interface{}) js.Value {
switch s := s.(type) {
case []int8:
a := js.Global().Get("Uint8Array").New(len(s))
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Int8Array").New(buf, a.Get("byteOffset"), a.Get("byteLength"))
case []int16:
a := js.Global().Get("Uint8Array").New(len(s) * 2)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Int16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
case []int32:
a := js.Global().Get("Uint8Array").New(len(s) * 4)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Int32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
case []uint8:
a := js.Global().Get("Uint8Array").New(len(s))
js.CopyBytesToJS(a, s)
runtime.KeepAlive(s)
return a
case []uint16:
a := js.Global().Get("Uint8Array").New(len(s) * 2)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Uint16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
case []uint32:
a := js.Global().Get("Uint8Array").New(len(s) * 4)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Uint32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
case []float32:
a := js.Global().Get("Uint8Array").New(len(s) * 4)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
case []float64:
a := js.Global().Get("Uint8Array").New(len(s) * 8)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get("buffer")
return js.Global().Get("Float64Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/8)
default:
panic("jsutil: unexpected value at SliceToTypedArray: " + reflect.TypeOf(s).String())
}
}
const controlsHTML = `
<datalist id="speeds">
<option>-{{.XRange}}</option>
<option>0</option>
<option>{{.XRange}}</option>
</datalist>
<table>
<tr>
<td>
<h2>3D Model</h2>
</td>
<td>
<p>X
<input id="speedSliderX" type="range" min="-{{.XRange}}" max="{{.XRange}}" step="{{.XStep}}" list="speeds">
<text id="speedSliderXValue">00.00</text>
</p>
</td>
<td>
<p>Y
<input id="speedSliderY" type="range" min="-{{.YRange}}" max="{{.YRange}}" step="{{.YStep}}" list="speeds">
<text id="speedSliderYValue">00.00</text>
</p>
</td>
<td>
<p>Z
<input id="speedSliderZ" type="range" min="-{{.ZRange}}" max="{{.ZRange}}" step="{{.ZStep}}" list="speeds">
<text id="speedSliderZValue">00.00</text>
</p>
</td>
<td>
<p>Zoom
<input id="speedSliderZoom" type="range" min="{{.ZMin}}" max="{{.ZMax}}" step="{{.ZoomStep}}" list="speeds">
<text id="speedSliderZoomValue">0000.00</text>
</p>
</td>
<td>
<p><button type="button" id="stop">Stop Rendering</button></p>
</td>
</tr>
</table>
`