// 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>
`