├── images
├── arc.png
├── screenshot01.png
└── screenshot02.png
├── models
├── Saw.glb
├── Buggy.glb
├── Chopper.glb
├── Cathedral.glb
├── Cesium_Air.glb
├── airbus
│ ├── scene.bin
│ ├── textures
│ │ └── Textures_1_baseColor.png
│ └── scene.gltf
└── Chopper_Texture.glb
├── .vscode
└── settings.json
├── Makefile
├── .gitignore
├── renderer
├── byteGraph_test.go
├── message.go
├── camera_test.go
├── command_test.go
├── byteGraph.go
├── gltfloader.go
├── selection.go
├── imagestream.go
├── camera.go
├── renderer.go
└── command.go
├── static
├── style.css
├── webg3n.js
├── popper.min.js
└── bootstrap.min.js
├── Dockerfile
├── LICENSE
├── go.mod
├── main.go
├── server.go
├── README.md
├── templates
└── webg3n.html
└── go.sum
/images/arc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/images/arc.png
--------------------------------------------------------------------------------
/models/Saw.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/Saw.glb
--------------------------------------------------------------------------------
/models/Buggy.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/Buggy.glb
--------------------------------------------------------------------------------
/models/Chopper.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/Chopper.glb
--------------------------------------------------------------------------------
/models/Cathedral.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/Cathedral.glb
--------------------------------------------------------------------------------
/models/Cesium_Air.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/Cesium_Air.glb
--------------------------------------------------------------------------------
/images/screenshot01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/images/screenshot01.png
--------------------------------------------------------------------------------
/images/screenshot02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/images/screenshot02.png
--------------------------------------------------------------------------------
/models/airbus/scene.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/airbus/scene.bin
--------------------------------------------------------------------------------
/models/Chopper_Texture.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/Chopper_Texture.glb
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "go.formatFlags": [
3 | "[\"-s\"]"
4 | ],
5 | "go.formatTool": "gofmt"
6 | }
--------------------------------------------------------------------------------
/models/airbus/textures/Textures_1_baseColor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moethu/webg3n/HEAD/models/airbus/textures/Textures_1_baseColor.png
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: build
2 |
3 | run:
4 | docker run --rm -d -p 8000:8000 local/webg3n
5 |
6 | build:
7 | docker build -t local/webg3n -f Dockerfile .
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | vendor/
16 |
17 | # binary
18 | webg3n
--------------------------------------------------------------------------------
/renderer/byteGraph_test.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestSlice(t *testing.T) {
8 | byteBuffer = []int{1, 2, 3, 4, 5, 6}
9 | AddToByteBuffer(7)
10 | assert(t, byteBuffer[5], 7)
11 | assert(t, len(byteBuffer), 6)
12 | AddToByteBuffer(8)
13 | assert(t, byteBuffer[5], 8)
14 | assert(t, len(byteBuffer), 6)
15 | AddToByteBuffer(9)
16 | assert(t, byteBuffer[5], 9)
17 | assert(t, len(byteBuffer), 6)
18 | }
19 |
--------------------------------------------------------------------------------
/static/style.css:
--------------------------------------------------------------------------------
1 |
2 | main > .container {
3 | padding: 60px 15px 0;
4 | }
5 |
6 | .footer {
7 | background-color: #f5f5f5;
8 | position: fixed;
9 | bottom: 0;
10 | width: 100%;
11 | }
12 |
13 | .header {
14 | position: relative;
15 | top: 0;
16 | width: 100%;
17 | }
18 | .container-for-canvas {
19 | position: absolute;
20 | width: 100%;
21 | height: 100%;
22 | padding: 70px 50px 55px 50px;
23 | }
24 | .footer > .container {
25 | padding-right: 15px;
26 | padding-left: 15px;
27 | }
28 |
29 | code {
30 | font-size: 80%;
31 | }
--------------------------------------------------------------------------------
/renderer/message.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import "encoding/json"
4 |
5 | // Message for client
6 | type Message struct {
7 | Action string `json:"action"`
8 | Value string `json:"value"`
9 | }
10 |
11 | // sendMessageToClient sends a message to the client
12 | func (app *RenderingApp) sendMessageToClient(action string, value string) {
13 | m := &Message{Action: action, Value: value}
14 | msgJSON, err := json.Marshal(m)
15 | if err != nil {
16 | app.Application.Log().Error(err.Error())
17 | return
18 | }
19 | app.Log().Info("sending message: " + string(msgJSON))
20 | app.cImagestream <- []byte(string(msgJSON))
21 | }
22 |
--------------------------------------------------------------------------------
/renderer/camera_test.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/g3n/engine/math32"
7 | )
8 |
9 | func TestGetCenter(t *testing.T) {
10 | b := math32.Box3{Min: math32.Vector3{X: 0, Y: 0, Z: 0}, Max: math32.Vector3{X: 10, Y: 10, Z: 100}}
11 | v := getCenter(b)
12 | if v.X != 5 || v.Y != 5 || v.Z != 50 {
13 | t.Error("Not Centered")
14 | }
15 | }
16 |
17 | func TestGetViewVectorByName(t *testing.T) {
18 | v := getViewVectorByName("top")
19 | if v.Y <= 0 {
20 | t.Error("top view incorrect")
21 | }
22 | v = getViewVectorByName("bottom")
23 | if v.Y >= 0 {
24 | t.Error("bottom view incorrect")
25 | }
26 | v = getViewVectorByName("left")
27 | if v.X <= 0 {
28 | t.Error("left view incorrect")
29 | }
30 | v = getViewVectorByName("right")
31 | if v.X >= 0 {
32 | t.Error("right view incorrect")
33 | }
34 | v = getViewVectorByName("rear")
35 | if v.Z >= 0 {
36 | t.Error("rear view incorrect")
37 | }
38 | v = getViewVectorByName("front")
39 | if v.Z <= 0 {
40 | t.Error("front view incorrect")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/renderer/command_test.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/g3n/engine/window"
7 | )
8 |
9 | func assert(t *testing.T, actual interface{}, expected interface{}) {
10 | if actual != expected {
11 | t.Error("Actual:", actual, "Expected:", expected)
12 | }
13 | }
14 | func TestMapMouseButton(t *testing.T) {
15 | assert(t, mapMouseButton("0"), window.MouseButtonLeft)
16 | assert(t, mapMouseButton("1"), window.MouseButtonMiddle)
17 | assert(t, mapMouseButton("2"), window.MouseButtonRight)
18 | assert(t, mapMouseButton("3"), window.MouseButtonLeft)
19 | }
20 |
21 | func TestMapKey(t *testing.T) {
22 | assert(t, mapKey("38"), window.KeyUp)
23 | assert(t, mapKey("37"), window.KeyLeft)
24 | assert(t, mapKey("39"), window.KeyRight)
25 | assert(t, mapKey("40"), window.KeyDown)
26 | assert(t, mapKey("41"), window.KeyEnter)
27 | }
28 |
29 | func TestGetValueInRange(t *testing.T) {
30 | assert(t, getValueInRange(6, 1, 5), 5)
31 | assert(t, getValueInRange(3, 1, 5), 3)
32 | assert(t, getValueInRange(0, 1, 5), 1)
33 | }
34 |
--------------------------------------------------------------------------------
/renderer/byteGraph.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "image"
5 | "image/color"
6 |
7 | "github.com/llgcode/draw2d/draw2dimg"
8 | )
9 |
10 | // byte buffer holding sent bytes
11 | var byteBuffer []int
12 |
13 | // AddToByteBuffer adding sent bytes to the history stack
14 | func AddToByteBuffer(value int) {
15 | byteBuffer = byteBuffer[1:]
16 | byteBuffer = append(byteBuffer, value)
17 | }
18 |
19 | // DrawByteGraph draws the histroy of sent bytes onto the image
20 | func DrawByteGraph(img *image.RGBA) *image.RGBA {
21 | if byteBuffer == nil {
22 | byteBuffer = make([]int, 50)
23 | }
24 | gc := draw2dimg.NewGraphicContext(img)
25 | x := float64(len(byteBuffer)) * 3.0
26 | y := float64(img.Bounds().Dy())
27 | gc.SetStrokeColor(color.RGBA{R: 255, G: 0, B: 0, A: 255})
28 | gc.SetLineWidth(0.5)
29 |
30 | // draw 100 kb line
31 | gc.BeginPath()
32 | gc.MoveTo(0, y-100)
33 | gc.LineTo(x, y-100)
34 | gc.Close()
35 | gc.Stroke()
36 |
37 | gc.SetStrokeColor(color.Black)
38 | gc.SetLineWidth(0.5)
39 | // draw sent bytes as bars in kb
40 | for _, value := range byteBuffer {
41 | gc.BeginPath()
42 | gc.MoveTo(x, y)
43 | gc.LineTo(x, y-float64(value/1000))
44 | gc.Close()
45 | gc.Stroke()
46 | x = x - 3.0
47 | }
48 | return img
49 | }
50 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # ######################################################################
2 | # Docker container
3 | # ######################################################################
4 | FROM ubuntu:latest
5 | LABEL maintainer="Bernhard Reitinger
"
6 |
7 | ENV GO111MODULE=on
8 |
9 | RUN apt-get update
10 | ENV TZ=Europe/Vienna
11 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
12 | RUN apt-get install -y xorg-dev libgl1-mesa-dev libopenal1 libopenal-dev libvorbis0a libvorbis-dev libvorbisfile3
13 | RUN apt-get install libjpeg-turbo8 libjpeg-turbo8-dev
14 | RUN apt-get install -y xvfb
15 |
16 | RUN apt-get install -y golang-1.14-go
17 | RUN apt-get install -y ca-certificates
18 |
19 | ENV PATH=$PATH:/usr/lib/go-1.14/bin
20 |
21 | WORKDIR /go/src/app
22 |
23 | COPY go.mod .
24 | COPY go.sum .
25 |
26 | RUN go mod download
27 |
28 | COPY . .
29 |
30 | RUN mkdir -p /go/bin
31 |
32 | RUN GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /go/bin/web-app
33 |
34 | WORKDIR /go/bin
35 |
36 | RUN cp -r /go/src/app/templates /go/bin
37 | RUN cp -r /go/src/app/static /go/bin
38 | RUN cp -r /go/src/app/models /go/bin
39 |
40 | EXPOSE 8000
41 |
42 | ENTRYPOINT ["/bin/sh", "-c", "/usr/bin/xvfb-run -s \"-screen 0 1920x1080x24\" -a $@", ""]
43 | CMD ["/go/bin/web-app"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019,2020 Maximilian Thumfart. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/renderer/gltfloader.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "fmt"
5 | "github.com/g3n/engine/core"
6 | "github.com/g3n/engine/loader/gltf"
7 | "path/filepath"
8 | "strconv"
9 | )
10 |
11 | // nameChildren names all gltf nodes by path
12 | func (app *RenderingApp) nameChildren(p string, n core.INode) {
13 | node := n.GetNode()
14 | node.SetName(p)
15 | app.nodeBuffer[p] = node
16 | for _, child := range node.Children() {
17 | idx := node.ChildIndex(child)
18 | title := p + "/" + strconv.Itoa(idx)
19 | app.nameChildren(title, child)
20 | }
21 | }
22 |
23 | // loadScene loads a gltf file
24 | func (app *RenderingApp) loadScene(fpath string) error {
25 | app.sendMessageToClient("loading", fpath)
26 | // Checks file extension
27 | ext := filepath.Ext(fpath)
28 | var g *gltf.GLTF
29 | var err error
30 |
31 | // Parses file
32 | if ext == ".gltf" {
33 | g, err = gltf.ParseJSON(fpath)
34 | } else if ext == ".glb" {
35 | g, err = gltf.ParseBin(fpath)
36 | } else {
37 | return fmt.Errorf("unrecognized file extension:%s", ext)
38 | }
39 |
40 | if err != nil {
41 | return err
42 | }
43 |
44 | defaultSceneIdx := 0
45 | if g.Scene != nil {
46 | defaultSceneIdx = *g.Scene
47 | }
48 |
49 | // Create default scene
50 | n, err := g.LoadScene(defaultSceneIdx)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | app.Scene().Add(n)
56 | root := app.Scene().ChildIndex(n)
57 | app.nameChildren("/"+strconv.Itoa(root), n)
58 | app.sendMessageToClient("loaded", fpath)
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/moethu/webg3n
2 |
3 | require (
4 | github.com/dsnet/compress v0.0.1 // indirect
5 | github.com/g3n/engine v0.1.0
6 | github.com/gin-gonic/gin v1.7.0
7 | github.com/golang/snappy v0.0.2 // indirect
8 | github.com/gorilla/websocket v1.4.2
9 | github.com/llgcode/draw2d v0.0.0-20200603164053-19660b984a28
10 | github.com/mholt/archiver v3.1.1+incompatible // indirect
11 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
12 | github.com/modern-go/reflect2 v1.0.1 // indirect
13 | github.com/moethu/imaging v1.6.3
14 | github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 // indirect
15 | github.com/nickalie/go-mozjpegbin v0.0.0-20170427050522-d8a58e243a3d
16 | github.com/nwaples/rardecode v1.1.0 // indirect
17 | github.com/pierrec/lz4 v2.6.0+incompatible // indirect
18 | github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d
19 | github.com/satori/go.uuid v1.2.0
20 | github.com/ulikunitz/xz v0.5.9 // indirect
21 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
22 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
23 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
24 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
25 | gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
26 | )
27 |
28 | replace github.com/moethu/webg3n/renderer => ./renderer
29 |
30 | replace github.com/moethu/webg3n/byteGraph => ./byteGraph
31 |
32 | replace github.com/g3n/engine => github.com/moethu/engine v0.0.0-20200610122637-682e1e061a29
33 |
34 | go 1.13
35 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "log"
7 | "net/http"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "text/template"
12 | "time"
13 |
14 | "github.com/g3n/engine/window"
15 |
16 | "github.com/gin-gonic/gin"
17 | "github.com/gorilla/websocket"
18 | )
19 |
20 | func main() {
21 | flag.Parse()
22 | log.SetFlags(0)
23 |
24 | router := gin.Default()
25 | port := ":8000"
26 | srv := &http.Server{
27 | Addr: port,
28 | Handler: router,
29 | ReadTimeout: 600 * time.Second,
30 | WriteTimeout: 600 * time.Second,
31 | }
32 |
33 | router.Static("/static/", "./static/")
34 | router.Any("/webg3n", serveWebsocket)
35 | router.GET("/", home)
36 | log.Println("Starting HTTP Server on Port 8000")
37 |
38 | go func() {
39 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
40 | log.Fatalf("listen: %s\n", err)
41 | }
42 | }()
43 |
44 | // Wait for interrupt signal to gracefully shutdown the server with
45 | // a timeout of 5 seconds.
46 | quit := make(chan os.Signal, 1)
47 | // kill (no param) default send syscanll.SIGTERM
48 | // kill -2 is syscall.SIGINT
49 | // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
50 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
51 | <-quit
52 | log.Println("Shutdown Server")
53 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
54 | defer cancel()
55 |
56 | if err := srv.Shutdown(ctx); err != nil {
57 | log.Fatal("Server Shutdown: ", err)
58 | }
59 |
60 | window.DestroyGlfwManager()
61 | log.Println("Server exiting")
62 | }
63 |
64 | var upgrader = websocket.Upgrader{}
65 |
66 | // Home route, loading template and serving it
67 | func home(c *gin.Context) {
68 | viewertemplate := template.Must(template.ParseFiles("templates/webg3n.html"))
69 | viewertemplate.Execute(c.Writer, c.Request.Host)
70 | }
71 |
--------------------------------------------------------------------------------
/renderer/selection.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "github.com/g3n/engine/core"
5 | "github.com/g3n/engine/graphic"
6 | "github.com/g3n/engine/math32"
7 | )
8 |
9 | // selectNode uses a raycaster to get the selected node.
10 | // It sends the selection as json to the image channel
11 | // and changes the node's material
12 | func (app *RenderingApp) selectNode(mx float32, my float32, multiselect bool) {
13 | width, height := app.Window().Size()
14 | x := (-.5 + mx/float32(width)) * 2.0
15 | y := (.5 - my/float32(height)) * 2.0
16 | app.Log().Info("click: %f, %f", x, y)
17 | r := core.NewRaycaster(&math32.Vector3{}, &math32.Vector3{})
18 | app.CameraPersp().SetRaycaster(r, x, y)
19 |
20 | i := r.IntersectObject(app.Scene(), true)
21 |
22 | var object *core.Node
23 | if len(i) != 0 {
24 | object = i[0].Object.GetNode()
25 | app.Log().Info("selected: %s", object.Name())
26 | app.sendMessageToClient("selected", object.Name())
27 | if !multiselect {
28 | app.resetSelection()
29 | }
30 | app.changeNodeMaterial(i[0].Object)
31 | } else {
32 | if !multiselect {
33 | app.sendMessageToClient("selected", "")
34 | app.resetSelection()
35 | }
36 | }
37 | }
38 |
39 | // resetSelection resets selected nodes to their original state
40 | func (app *RenderingApp) resetSelection() {
41 | for inode, materials := range app.selectionBuffer {
42 | gnode, _ := inode.(graphic.IGraphic)
43 | gfx := gnode.GetGraphic()
44 | gfx.ClearMaterials()
45 | for _, material := range materials {
46 | gfx.AddMaterial(material.IGraphic(), material.IMaterial(), 0, 0)
47 | }
48 | delete(app.selectionBuffer, inode)
49 | }
50 | }
51 |
52 | // changeNodeMaterial changes a node's material to selected
53 | func (app *RenderingApp) changeNodeMaterial(inode core.INode) {
54 | gnode, ok := inode.(graphic.IGraphic)
55 |
56 | if ok {
57 | if gnode.Renderable() {
58 | gfx := gnode.GetGraphic()
59 | var materials []graphic.GraphicMaterial
60 | for _, material := range gfx.Materials() {
61 | materials = append(materials, material)
62 | }
63 | app.selectionBuffer[inode] = materials
64 | gfx.ClearMaterials()
65 | gfx.AddMaterial(gnode, app.selectionMaterial, 0, 0)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/renderer/imagestream.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "bytes"
5 | "crypto/md5"
6 | "encoding/base64"
7 | "image"
8 | "image/jpeg"
9 | "image/png"
10 |
11 | "github.com/moethu/imaging"
12 | libjpeg "github.com/pixiv/go-libjpeg/jpeg"
13 | )
14 |
15 | // onRender event handler for onRender event
16 | func (app *RenderingApp) onRender(evname string, ev interface{}) {
17 | app.makeScreenShot()
18 | }
19 |
20 | var md5SumBuffer [16]byte
21 |
22 | // makeScreenShot reads the opengl buffer, encodes it as jpeg and sends it to the channel
23 | func (app *RenderingApp) makeScreenShot() {
24 | w := app.Width
25 | h := app.Height
26 | data := app.Gl().ReadPixels(0, 0, w, h, 6408, 5121)
27 | img := image.NewRGBA(image.Rect(0, 0, w, h))
28 | img.Pix = data
29 |
30 | if app.imageSettings.getPixelation() > 1.0 {
31 | img = imaging.Fit(img, int(float64(w)/app.imageSettings.getPixelation()), int(float64(h)/app.imageSettings.getPixelation()), imaging.NearestNeighbor)
32 | }
33 | if app.imageSettings.brightness != 0 {
34 | img = imaging.AdjustBrightness(img, app.imageSettings.brightness)
35 | }
36 | if app.imageSettings.contrast != 0 {
37 | img = imaging.AdjustContrast(img, app.imageSettings.contrast)
38 | }
39 | if app.imageSettings.saturation != 0 {
40 | img = imaging.AdjustSaturation(img, app.imageSettings.saturation)
41 | }
42 | if app.imageSettings.blur != 0 {
43 | img = imaging.Blur(img, app.imageSettings.blur)
44 | }
45 | if app.imageSettings.invert {
46 | img = imaging.Invert(img)
47 | }
48 |
49 | img = imaging.FlipV(img)
50 |
51 | if app.Debug {
52 | img = DrawByteGraph(img)
53 | }
54 |
55 | buf := new(bytes.Buffer)
56 | var err interface{}
57 | switch app.imageSettings.encoder {
58 | case "png":
59 | err = png.Encode(buf, img)
60 | case "jpeg":
61 | var opt jpeg.Options
62 | opt.Quality = app.imageSettings.getJpegQuality()
63 | err = jpeg.Encode(buf, img, &opt)
64 | default:
65 | var opt libjpeg.EncoderOptions
66 | opt.Quality = app.imageSettings.getJpegQuality()
67 | err = libjpeg.Encode(buf, img, &opt)
68 | }
69 |
70 | if err != nil {
71 | panic(err)
72 | }
73 | imageBit := buf.Bytes()
74 |
75 | // get md5 checksum from image to check if image changed
76 | // only send a new image to the client if there has been any change.
77 | md := md5.Sum(imageBit)
78 | if md5SumBuffer != md {
79 | imgBase64Str := base64.StdEncoding.EncodeToString([]byte(imageBit))
80 | if app.Debug {
81 | AddToByteBuffer(len(imgBase64Str))
82 | }
83 | app.cImagestream <- []byte(imgBase64Str)
84 | md5SumBuffer = md
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/renderer/camera.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "github.com/g3n/engine/math32"
5 | "log"
6 | )
7 |
8 | // getCenter gets the centerpoint of a 3D box
9 | func getCenter(box math32.Box3) *math32.Vector3 {
10 | return box.Center(nil)
11 | }
12 |
13 | // focusOnSelection sets the camera focus on the current selection
14 | func (app *RenderingApp) focusOnSelection() {
15 | var bbox *math32.Box3
16 | first := true
17 | for inode := range app.selectionBuffer {
18 | tmp := inode.BoundingBox()
19 | if first {
20 | bbox = math32.NewBox3(&tmp.Min, &tmp.Max)
21 | log.Println(bbox)
22 | first = false
23 | } else {
24 | bbox.ExpandByPoint(&tmp.Min)
25 | bbox.ExpandByPoint(&tmp.Max)
26 | }
27 | }
28 | if first {
29 | return
30 | }
31 | position := app.Camera().GetCamera().Position()
32 | C := bbox.Center(nil)
33 | r := C.DistanceTo(&bbox.Max)
34 | a := app.CameraPersp().Fov()
35 | d := r / math32.Sin(a/2)
36 | P := math32.Vector3{X: C.X, Y: C.Y, Z: C.Z}
37 | dir := math32.Vector3{X: C.X, Y: C.Y, Z: C.Z}
38 | P.Add(((position.Sub(C)).Normalize().MultiplyScalar(d)))
39 | dir.Sub(&P)
40 | app.Camera().GetCamera().SetPositionVec(&P)
41 | app.Camera().GetCamera().LookAt(C)
42 | }
43 |
44 | // getViewVectorByName gets a view direction vector by name
45 | func getViewVectorByName(view string) math32.Vector3 {
46 | modifier := math32.Vector3{X: 0, Y: 0, Z: 0}
47 | switch view {
48 | case "top":
49 | modifier.Y = 10
50 | case "bottom":
51 | modifier.Y = -10
52 | case "front":
53 | modifier.Z = 10
54 | case "rear":
55 | modifier.Z = -10
56 | case "left":
57 | modifier.X = 10
58 | case "right":
59 | modifier.X = -10
60 | }
61 | return modifier
62 | }
63 |
64 | // setCamera set camera sets a camera standard view by name
65 | func (app *RenderingApp) setCamera(view string) {
66 | modifier := getViewVectorByName(view)
67 | bbox := app.Scene().ChildAt(0).BoundingBox()
68 | C := bbox.Center(nil)
69 | pos := modifier.Add(C)
70 | app.focusCameraToCenter(*pos)
71 | }
72 |
73 | // focusCameraToCenter sets the camera focus to the center of the entire model
74 | func (app *RenderingApp) focusCameraToCenter(position math32.Vector3) {
75 | bbox := app.Scene().ChildAt(0).BoundingBox()
76 | C := bbox.Center(nil)
77 | r := C.DistanceTo(&bbox.Max)
78 | a := app.CameraPersp().Fov()
79 | d := r / math32.Sin(a/2)
80 | P := math32.Vector3{X: C.X, Y: C.Y, Z: C.Z}
81 | dir := math32.Vector3{X: C.X, Y: C.Y, Z: C.Z}
82 | P.Add(((position.Sub(C)).Normalize().MultiplyScalar(d)))
83 | dir.Sub(&P)
84 | app.Camera().GetCamera().SetPositionVec(&P)
85 | app.Camera().GetCamera().LookAt(C)
86 | }
87 |
88 | // zoomToExtent zooms the view to extent
89 | func (app *RenderingApp) zoomToExtent() {
90 | pos := app.Camera().GetCamera().Position()
91 | app.focusCameraToCenter(pos)
92 | }
93 |
--------------------------------------------------------------------------------
/renderer/renderer.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/g3n/engine/core"
7 | "github.com/g3n/engine/graphic"
8 | "github.com/g3n/engine/light"
9 |
10 | "github.com/g3n/engine/material"
11 | "github.com/g3n/engine/math32"
12 | "github.com/g3n/engine/util/application"
13 | "github.com/g3n/engine/util/logger"
14 | )
15 |
16 | // ImageSettings for rendering image
17 | type ImageSettings struct {
18 | saturation float64
19 | contrast float64
20 | brightness float64
21 | blur float64
22 | pixelation float64
23 | invert bool
24 | quality Quality
25 | isNavigating bool
26 | encoder string
27 | }
28 |
29 | // getJpegQuality returns quality depending on navigation movement
30 | func (i *ImageSettings) getJpegQuality() int {
31 | if i.isNavigating {
32 | return i.quality.jpegQualityNav
33 | } else {
34 | return i.quality.jpegQualityStill
35 | }
36 | }
37 |
38 | // getPixelation returns pixelation depending on navigation movement
39 | // A global pixelation level will override preset pixelation levels
40 | func (i *ImageSettings) getPixelation() float64 {
41 | if i.pixelation > 1.0 {
42 | return i.pixelation
43 | }
44 | if i.isNavigating {
45 | return i.quality.pixelationNav
46 | } else {
47 | return i.quality.pixelationStill
48 | }
49 | }
50 |
51 | // Quality Image quality settings for still and navigating situations
52 | type Quality struct {
53 | jpegQualityStill int
54 | jpegQualityNav int
55 | pixelationStill float64
56 | pixelationNav float64
57 | }
58 |
59 | // high image quality definition
60 | var highQ Quality = Quality{jpegQualityStill: 100, jpegQualityNav: 90, pixelationStill: 1.0, pixelationNav: 1.0}
61 |
62 | // medium image quality definition
63 | var mediumQ Quality = Quality{jpegQualityStill: 80, jpegQualityNav: 60, pixelationStill: 1.0, pixelationNav: 1.2}
64 |
65 | // low image quality definition
66 | var lowQ Quality = Quality{jpegQualityStill: 60, jpegQualityNav: 40, pixelationStill: 1.0, pixelationNav: 1.5}
67 |
68 | // RenderingApp application settings
69 | type RenderingApp struct {
70 | application.Application
71 | x, y, z float32
72 | cImagestream chan []byte
73 | cCommands chan []byte
74 | Width int
75 | Height int
76 | imageSettings ImageSettings
77 | selectionBuffer map[core.INode][]graphic.GraphicMaterial
78 | selectionMaterial material.IMaterial
79 | modelpath string
80 | nodeBuffer map[string]*core.Node
81 | Debug bool
82 | }
83 |
84 | // LoadRenderingApp loads the rendering application
85 | func LoadRenderingApp(app *RenderingApp, sessionId string, h int, w int, write chan []byte, read chan []byte, modelpath string) {
86 | a, err := application.Create(application.Options{
87 | Title: "g3nServerApplication",
88 | Width: w,
89 | Height: h,
90 | Fullscreen: false,
91 | LogPrefix: sessionId,
92 | LogLevel: logger.DEBUG,
93 | TargetFPS: 30,
94 | EnableFlags: true,
95 | })
96 |
97 | if err != nil {
98 | panic(err)
99 | }
100 |
101 | app.Application = *a
102 | app.Width = w
103 | app.Height = h
104 |
105 | app.imageSettings = ImageSettings{
106 | saturation: 0,
107 | brightness: 0,
108 | contrast: 0,
109 | blur: 0,
110 | pixelation: 1.0,
111 | invert: false,
112 | quality: highQ,
113 | encoder: "libjpeg",
114 | }
115 |
116 | app.cImagestream = write
117 | app.cCommands = read
118 | app.modelpath = modelpath
119 | app.setupScene()
120 | go app.commandLoop()
121 | err = app.Run()
122 | if err != nil {
123 | panic(err)
124 | }
125 |
126 | app.Log().Info("app was running for %f seconds\n", app.RunSeconds())
127 | }
128 |
129 | // setupScene sets up the current scene
130 | func (app *RenderingApp) setupScene() {
131 | app.selectionMaterial = material.NewPhong(math32.NewColor("Red"))
132 | app.selectionBuffer = make(map[core.INode][]graphic.GraphicMaterial)
133 | app.nodeBuffer = make(map[string]*core.Node)
134 |
135 | app.Gl().ClearColor(1.0, 1.0, 1.0, 1.0)
136 |
137 | er := app.loadScene(app.modelpath)
138 | if er != nil {
139 | log.Fatal(er)
140 | }
141 |
142 | amb := light.NewAmbient(&math32.Color{R: 0.2, G: 0.2, B: 0.2}, 1.0)
143 | app.Scene().Add(amb)
144 |
145 | plight := light.NewPoint(math32.NewColor("white"), 40)
146 | plight.SetPosition(100, 20, 70)
147 | plight.SetLinearDecay(.001)
148 | plight.SetQuadraticDecay(.001)
149 | app.Scene().Add(plight)
150 |
151 | app.Camera().GetCamera().SetPosition(12, 1, 5)
152 |
153 | p := math32.Vector3{X: 0, Y: 0, Z: 0}
154 | app.Camera().GetCamera().LookAt(&p)
155 | app.CameraPersp().SetFov(50)
156 | app.zoomToExtent()
157 | app.Orbit().Enabled = true
158 | app.Application.Subscribe(application.OnAfterRender, app.onRender)
159 | }
160 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/moethu/webg3n/renderer"
10 |
11 | "github.com/gin-gonic/gin"
12 | "github.com/gorilla/websocket"
13 | uuid "github.com/satori/go.uuid"
14 | )
15 |
16 | const (
17 | writeTimeout = 10 * time.Second
18 | readTimeout = 60 * time.Second
19 | pingPeriod = (readTimeout * 9) / 10
20 | maxMessageSize = 512
21 | )
22 |
23 | // Client holding g3napp, socket and channels
24 | type Client struct {
25 | app renderer.RenderingApp
26 |
27 | // The websocket connection.
28 | conn *websocket.Conn
29 |
30 | // Buffered channels messages.
31 | write chan []byte // images and data to client
32 | read chan []byte // commands from client
33 | }
34 |
35 | // streamReader reads messages from the websocket connection and fowards them to the read channel
36 | func (c *Client) streamReader() {
37 | defer func() {
38 | c.conn.Close()
39 | }()
40 | c.conn.SetReadLimit(maxMessageSize)
41 | c.conn.SetReadDeadline(time.Now().Add(readTimeout))
42 | // SetPongHandler sets the handler for pong messages received from the peer.
43 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(readTimeout)); return nil })
44 | for {
45 | _, message, err := c.conn.ReadMessage()
46 | if err != nil {
47 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
48 | log.Printf("error: %v", err)
49 | }
50 | break
51 | }
52 | // feed message to command channel
53 | c.read <- message
54 | }
55 | }
56 |
57 | // streamWriter writes messages from the write channel to the websocket connection
58 | func (c *Client) streamWriter() {
59 | ticker := time.NewTicker(pingPeriod)
60 | defer func() {
61 | ticker.Stop()
62 | c.conn.Close()
63 | }()
64 | for {
65 | // Go’s select lets you wait on multiple channel operations.
66 | // We’ll use select to await both of these values simultaneously.
67 | select {
68 | case message, ok := <-c.write:
69 | c.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
70 | if !ok {
71 | c.conn.WriteMessage(websocket.CloseMessage, []byte{})
72 | return
73 | }
74 |
75 | // NextWriter returns a writer for the next message to send.
76 | // The writer's Close method flushes the complete message to the network.
77 | w, err := c.conn.NextWriter(websocket.TextMessage)
78 | if err != nil {
79 | return
80 | }
81 | w.Write(message)
82 |
83 | // Add queued messages to the current websocket message
84 | n := len(c.write)
85 | for i := 0; i < n; i++ {
86 | w.Write(<-c.write)
87 | }
88 |
89 | if err := w.Close(); err != nil {
90 | return
91 | }
92 |
93 | //a channel that will send the time with a period specified by the duration argument
94 | case <-ticker.C:
95 | // SetWriteDeadline sets the deadline for future Write calls
96 | // and any currently-blocked Write call.
97 | // Even if write times out, it may return n > 0, indicating that
98 | // some of the data was successfully written.
99 | c.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
100 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
101 | return
102 | }
103 | }
104 | }
105 | }
106 |
107 | // serveWebsocket handles websocket requests from the peer.
108 | func serveWebsocket(c *gin.Context) {
109 | sessionId := uuid.NewV4()
110 | // upgrade connection to websocket
111 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
112 | if err != nil {
113 | log.Println(err)
114 | return
115 | }
116 | conn.EnableWriteCompression(true)
117 |
118 | // create two channels for read write concurrency
119 | cWrite := make(chan []byte)
120 | cRead := make(chan []byte)
121 |
122 | client := &Client{conn: conn, write: cWrite, read: cRead}
123 |
124 | // get scene width and height from url query params
125 | // default to 800 if they are not set
126 | height := getParameterDefault(c, "h", 800)
127 | width := getParameterDefault(c, "w", 800)
128 |
129 | modelPath := "models/"
130 | defaultModel := "Cathedral.glb"
131 | model := c.Request.URL.Query().Get("model")
132 | if model == "" {
133 | model = defaultModel
134 | }
135 | if _, err := os.Stat(modelPath + model); os.IsNotExist(err) {
136 | model = defaultModel
137 | }
138 |
139 | // run 3d application in separate go routine
140 | go renderer.LoadRenderingApp(&client.app, sessionId.String(), height, width, cWrite, cRead, modelPath+model)
141 |
142 | // run reader and writer in two different go routines
143 | // so they can act concurrently
144 | go client.streamReader()
145 | go client.streamWriter()
146 | }
147 |
148 | // getParameterDefault gets a parameter and returns default value if its not set
149 | func getParameterDefault(c *gin.Context, name string, defaultValue int) int {
150 | val, err := strconv.Atoi(c.Request.URL.Query().Get(name))
151 | if err != nil {
152 | log.Println(err)
153 | return defaultValue
154 | }
155 | return val
156 | }
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # webg3n
3 |
4 | webg3n is a 3D web-viewer running [G3N](https://github.com/g3n/engine) as a server side OpenGL renderer.
5 |
6 | [](https://goreportcard.com/report/github.com/moethu/webg3n)
7 |
8 | 
9 |
10 | [Checkout this demo video](https://vimeo.com/358812535)
11 |
12 | ## How it works
13 |
14 | 
15 |
16 | webg3n is a GO webserver handling sockets to one G3N rendering instance per connection. The G3N application is constantly streaming images (jpeg) and listens for navigation events and commands from the client. The server supports multiple clients at the same time. For each client it will open one socket and spin off a separate 3D engine for rendering. The G3N application is reading [GLTF models](https://github.com/KhronosGroup/glTF), the example model is taken from [here](https://github.com/KhronosGroup/glTF-Sample-Models)
17 |
18 | ## Why Server Side Rendering
19 |
20 | Client and server side renderers have both pros and cons. Depending on your use case server side rendering of 3D models can be a great alternative to client side WebGL rendering. Here are just a few reasons why:
21 |
22 | - Browser doesn't need to support WebGL (older Browsers)
23 | - Smaller and weaker devices might not be able to render large models on client side
24 | - The geometry remains on the server and is not transferred to the client
25 |
26 | On the other hand it shifts the bottleneck from the client's rendering capabilites to the bandwith.
27 |
28 | ## Image Streaming Approaches
29 |
30 | This project has various branches to investigate different streaming approaches:
31 |
32 | - The default branch `master` streams base64 encoded image contents. This works very fast and has very little delay.
33 | - The `binary-image-transfer` branch streams binary image content (uint8Array). Slightly less data to transfer but adds some delay.
34 | - The `video-jmux` branch streams H264 encoded video stream to a video tag (also as uint8Array) using [jmuxer](https://github.com/samirkumardas/jmuxer). Image transitions are very smooth but this also adds some delay.
35 |
36 | ## Dependencies and Installation
37 |
38 | ### Docker
39 |
40 | The easies way to run webg3n is to `build` and `run` a container using the supplied Dockerfile.
41 |
42 | ### Not using Docker?
43 |
44 | Go 1.8+ is required. The engine also requires the system to have an OpenGL driver and a GCC-compatible C compiler.
45 |
46 | Requires this modified [G3N engine](https://github.com/moethu/engine) which gets installed via go modules.
47 |
48 | On Unix-based systems the engine depends on some C libraries that can be installed using the appropriate distribution package manager. See below for OS specific requirements.
49 |
50 | #### Ubuntu/Debian-like
51 |
52 | ```
53 | $ sudo apt-get install xorg-dev libgl1-mesa-dev libopenal1 libopenal-dev libvorbis0a libvorbis-dev libvorbisfile3 libjpeg-turbo8 libjpeg-turbo8-dev
54 | ```
55 |
56 | #### Fedora
57 |
58 | ```
59 | $ sudo dnf -y install xorg-x11-proto-devel mesa-libGL mesa-libGL-devel openal-soft openal-soft-devel libvorbis libvorbis-devel glfw-devel libXi-devel libjpeg-turbo8 libjpeg-turbo8-devel
60 | ```
61 |
62 | #### CentOS 7
63 |
64 | Enable the EPEL repository:
65 | ```
66 | $ sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
67 | ```
68 | Then install the same packages as for Fedora - remember to use yum instead of dnf for the package installation command.
69 |
70 | #### Windows
71 |
72 | The necessary audio libraries sources and DLLs are supplied but they need to be installed manually. Please see Audio libraries for Windows for details. We tested the Windows build using the mingw-w64 toolchain (you can download this file in particular).
73 |
74 | #### macOS
75 |
76 | Install the development files of OpenAL and Vorbis using Homebrew:
77 | ```
78 | brew install libvorbis openal-soft
79 | ```
80 | Important Note: OpenGL has been deprecated on MacOS.
81 | If you are experiencing issues building webg3n please read the following issue:
82 | https://github.com/moethu/webg3n/issues/18
83 | You might be able to acitvate it using this hint:
84 | https://www.alora.io/forums/topic/28972-how-to-get-hd-opengl-working-on-mac-osx-high-sierra-mojave/
85 |
86 | ## Example
87 |
88 | This implementation comes with a simple webUI, simply go run it and connect to port 8000.
89 | Once you click connect, a g3n window will appear which is the server side rendering screen.
90 |
91 | ## Features
92 |
93 | - mouse navigation
94 | - keyboard navigation
95 | - multi-select elements
96 | - hide and unhide element selection
97 | - focus on element selection
98 | - zoom extents
99 | - default views (top, down, left, right, front, rear)
100 | - change field of view
101 | - set compression quality
102 | - automatically set higher compression while navigating
103 | - adjust image settings (invert, brightness, contrast, saturation, blur)
104 |
105 | ## Contributing
106 |
107 | If you find a bug or create a new feature you are encouraged to send pull requests!
108 |
--------------------------------------------------------------------------------
/templates/webg3n.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
119 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/renderer/command.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "reflect"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/g3n/engine/window"
11 | )
12 |
13 | // Command received from client
14 | type Command struct {
15 | X float32
16 | Y float32
17 | Cmd string
18 | Val string
19 | Moved bool
20 | Ctrl bool
21 | }
22 |
23 | // mapMouseButton maps js mouse buttons to window mouse buttons
24 | func mapMouseButton(value string) window.MouseButton {
25 | switch value {
26 | case "0":
27 | return window.MouseButtonLeft
28 | case "1":
29 | return window.MouseButtonMiddle
30 | case "2":
31 | return window.MouseButtonRight
32 | default:
33 | return window.MouseButtonLeft
34 | }
35 | }
36 |
37 | // mapKey maps js keys to window keys
38 | func mapKey(value string) window.Key {
39 | switch value {
40 | case "38":
41 | return window.KeyUp
42 | case "37":
43 | return window.KeyLeft
44 | case "39":
45 | return window.KeyRight
46 | case "40":
47 | return window.KeyDown
48 | default:
49 | return window.KeyEnter
50 | }
51 | }
52 |
53 | // commandLoop listens for incoming commands and forwards them to the rendering app
54 | func (app *RenderingApp) commandLoop() {
55 | t := reflect.TypeOf(app)
56 | v := reflect.ValueOf(app)
57 | k := reflect.TypeOf(Command{}).Kind()
58 | for {
59 | message := <-app.cCommands
60 |
61 | // retrieve command data from payload
62 | cmd := Command{}
63 | err := json.Unmarshal(message, &cmd)
64 | if err != nil {
65 | app.Log().Error(err.Error())
66 | }
67 |
68 | // no command should be directed to orbit control
69 | if cmd.Cmd == "" {
70 | cmd.Cmd = "Navigate"
71 | } else {
72 | app.Log().Info("received command: %v", cmd)
73 | }
74 |
75 | // if a func with a matching command name exists,
76 | // call it with two args: the app itself and the command payload
77 | m, found := t.MethodByName(cmd.Cmd)
78 | if found {
79 | // make sure we got the right func with Command argument
80 | // otherwise Func.Call will panic
81 | if m.Type.NumIn() == 2 {
82 | if m.Type.In(1).Kind() == k {
83 | args := []reflect.Value{v, reflect.ValueOf(cmd)}
84 | m.Func.Call(args)
85 | }
86 | }
87 | } else {
88 | app.Log().Info("Unknown Command: " + cmd.Cmd)
89 | }
90 | }
91 | }
92 |
93 | // Navigate orbit navigation
94 | func (app *RenderingApp) Navigate(cmd Command) {
95 | cev := window.CursorEvent{Xpos: cmd.X, Ypos: cmd.Y}
96 | app.Orbit().OnCursorPos(&cev)
97 | }
98 |
99 | // Mousedown triggers a mousedown event
100 | func (app *RenderingApp) Mousedown(cmd Command) {
101 | mev := window.MouseEvent{Xpos: cmd.X, Ypos: cmd.Y,
102 | Action: window.Press,
103 | Button: mapMouseButton(cmd.Val)}
104 | if cmd.Moved {
105 | app.imageSettings.isNavigating = true
106 | }
107 | app.Orbit().OnMouse(&mev)
108 | }
109 |
110 | // Zoom in/out scene
111 | func (app *RenderingApp) Zoom(cmd Command) {
112 | scrollFactor := float32(10.0)
113 | mev := window.ScrollEvent{Xoffset: cmd.X, Yoffset: -cmd.Y / scrollFactor}
114 | app.Orbit().OnScroll(&mev)
115 | }
116 |
117 | // Mouseup event
118 | func (app *RenderingApp) Mouseup(cmd Command) {
119 | mev := window.MouseEvent{Xpos: cmd.X, Ypos: cmd.Y,
120 | Action: window.Release,
121 | Button: mapMouseButton(cmd.Val)}
122 |
123 | app.imageSettings.isNavigating = false
124 | app.Orbit().OnMouse(&mev)
125 |
126 | // mouse left click
127 | if cmd.Val == "0" && !cmd.Moved {
128 | app.selectNode(cmd.X, cmd.Y, cmd.Ctrl)
129 | }
130 | }
131 |
132 | // Hide selected element
133 | func (app *RenderingApp) Hide(cmd Command) {
134 | for inode := range app.selectionBuffer {
135 | inode.GetNode().SetVisible(false)
136 | }
137 | app.resetSelection()
138 | }
139 |
140 | // Unhide all hidden elements
141 | func (app *RenderingApp) Unhide(cmd Command) {
142 | for _, node := range app.nodeBuffer {
143 | node.SetVisible(true)
144 | }
145 | }
146 |
147 | // Send element userdata to client
148 | func (app *RenderingApp) Userdata(cmd Command) {
149 | if node, ok := app.nodeBuffer[cmd.Val]; ok {
150 | app.sendMessageToClient("userdata", fmt.Sprintf("%v", node.UserData()))
151 | }
152 | }
153 |
154 | // Keydown event
155 | func (app *RenderingApp) Keydown(cmd Command) {
156 | kev := window.KeyEvent{Action: window.Press, Mods: 0, Keycode: mapKey(cmd.Val)}
157 | app.Orbit().OnKey(&kev)
158 | }
159 |
160 | // Keyup event
161 | func (app *RenderingApp) Keyup(cmd Command) {
162 | kev := window.KeyEvent{Action: window.Release, Mods: 0, Keycode: mapKey(cmd.Val)}
163 | app.Orbit().OnKey(&kev)
164 | }
165 |
166 | // View sets standard views
167 | func (app *RenderingApp) View(cmd Command) {
168 | app.setCamera(cmd.Val)
169 | }
170 |
171 | // Zoomextent entire model
172 | func (app *RenderingApp) Zoomextent(cmd Command) {
173 | app.zoomToExtent()
174 | }
175 |
176 | // Focus on selection
177 | func (app *RenderingApp) Focus(cmd Command) {
178 | app.focusOnSelection()
179 | }
180 |
181 | // Invert image
182 | func (app *RenderingApp) Invert(cmd Command) {
183 | if app.imageSettings.invert {
184 | app.imageSettings.invert = false
185 | } else {
186 | app.imageSettings.invert = true
187 | }
188 | }
189 |
190 | // Imagesettings applies rendering settings
191 | func (app *RenderingApp) Imagesettings(cmd Command) {
192 | s := strings.Split(cmd.Val, ":")
193 | if len(s) == 5 {
194 | br, err := strconv.Atoi(s[0])
195 | if err == nil {
196 | app.imageSettings.brightness = float64(getValueInRange(br, -100, 100))
197 | }
198 | ct, err := strconv.Atoi(s[1])
199 | if err == nil {
200 | app.imageSettings.contrast = float64(getValueInRange(ct, -100, 100))
201 | }
202 | sa, err := strconv.Atoi(s[2])
203 | if err == nil {
204 | app.imageSettings.saturation = float64(getValueInRange(sa, -100, 100))
205 | }
206 | bl, err := strconv.Atoi(s[3])
207 | if err == nil {
208 | app.imageSettings.blur = float64(getValueInRange(bl, 0, 20))
209 | }
210 | pix, err := strconv.ParseFloat(s[4], 64)
211 | if err == nil {
212 | app.imageSettings.pixelation = getFloatValueInRange(pix, 1.0, 10.0)
213 | }
214 | }
215 | }
216 |
217 | // Quality settings
218 | func (app *RenderingApp) Quality(cmd Command) {
219 | quality, err := strconv.Atoi(cmd.Val)
220 | if err == nil {
221 | switch quality {
222 | case 0:
223 | app.imageSettings.quality = highQ
224 | case 2:
225 | app.imageSettings.quality = lowQ
226 | default:
227 | app.imageSettings.quality = mediumQ
228 | }
229 | }
230 | }
231 |
232 | // Enocder settings
233 | func (app *RenderingApp) Encoder(cmd Command) {
234 | app.imageSettings.encoder = cmd.Val
235 | }
236 |
237 | // Fov applies field of view
238 | func (app *RenderingApp) Fov(cmd Command) {
239 | fov, err := strconv.Atoi(cmd.Val)
240 | if err == nil {
241 | app.CameraPersp().SetFov(float32(getValueInRange(fov, 5, 120)))
242 | }
243 | }
244 |
245 | // Debugmode toggles bytegraph
246 | func (app *RenderingApp) Debugmode(cmd Command) {
247 | if app.Debug {
248 | app.Debug = false
249 | } else {
250 | app.Debug = true
251 | }
252 | }
253 |
254 | // Closes connection
255 | func (app *RenderingApp) Close(cmd Command) {
256 | app.Log().Info("close")
257 | app.Window().SetShouldClose(true)
258 | }
259 |
260 | // getValueInRange returns a value within bounds
261 | func getValueInRange(value int, lower int, upper int) int {
262 | if value > upper {
263 | return upper
264 | } else if value < lower {
265 | return lower
266 | } else {
267 | return value
268 | }
269 | }
270 |
271 | // getFloatValueInRange returns a value within bounds
272 | func getFloatValueInRange(value float64, lower float64, upper float64) float64 {
273 | if value > upper {
274 | return upper
275 | } else if value < lower {
276 | return lower
277 | } else {
278 | return value
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/static/webg3n.js:
--------------------------------------------------------------------------------
1 | let host = document.currentScript.getAttribute('host');
2 |
3 | $(document).ready(function () {
4 | console.log("ready!");
5 | fitToContainer(canvas)
6 | });
7 |
8 | function fitToContainer(canvas) {
9 | // Make it visually fill the positioned parent
10 | canvas.style.width = '100%';
11 | canvas.style.height = '100%';
12 | // ...then set the internal size to match
13 | canvas.width = canvas.offsetWidth;
14 | canvas.height = canvas.offsetHeight;
15 | }
16 |
17 | window.addEventListener("load", function (evt) {
18 | var canvas = document.getElementById("canvas");
19 | var spinner = document.getElementById("spinner");
20 | var selection_ui = document.getElementById("selection");
21 | var ws;
22 | var mouse_moved = false;
23 | var prev_x = undefined;
24 | var prev_y = undefined;
25 |
26 | spinner.style.display = 'none';
27 |
28 | var print = function (message) {
29 | console.log(message);
30 | };
31 |
32 | document.getElementById("open").onclick = function (evt) {
33 | if (ws) {
34 | return false;
35 | }
36 |
37 | h = $('#canvas').height();
38 | w = $('#canvas').width();
39 | console.log(h, w)
40 | ws = new WebSocket(`${host}?h=${h}&w=${w}`);
41 |
42 | ws.onopen = function (evt) {
43 | print("Connected to Server");
44 | }
45 | ws.onclose = function (evt) {
46 | print("Closed Connection");
47 | ws = null;
48 | }
49 | ws.onmessage = function (evt) {
50 | if (evt.data.startsWith('{') && evt.data.endsWith('}')) {
51 | var feedback = JSON.parse(evt.data);
52 | print(evt.data)
53 | if (feedback.action == "loaded") {
54 | spinner.style.display = 'none';
55 | }
56 | if (feedback.action == "loading") {
57 | spinner.style.display = 'block';
58 | }
59 | if (feedback.action == "selected") {
60 | if (feedback.value == "") {
61 | selection_ui.innerHTML = "No selection"
62 | } else {
63 | selection_ui.innerHTML = `Selected Node ${feedback.value}`;
64 | }
65 | }
66 | } else {
67 | var ctx = document.getElementById('canvas').getContext('2d');
68 | var img = new Image(w, h);
69 | img.onload = function () {
70 | ctx.drawImage(img, 0, 0, w, h);
71 | };
72 | img.src = 'data:image/jpeg;base64,' + evt.data;
73 | }
74 | }
75 | ws.onerror = function (evt) {
76 | print("Error: " + evt.data);
77 | }
78 | return false;
79 | };
80 |
81 | canvas.onmousemove = function (evt) {
82 | if (!ws) {
83 | return false;
84 | }
85 | mouse_moved = true;
86 | var rect = evt.target.getBoundingClientRect();
87 | var x = (evt.clientX - rect.left);
88 | var y = (evt.clientY - rect.top);
89 | ws.send(`{"x":${x},"y":${y}, "cmd":""}`);
90 | return false;
91 | }
92 |
93 | canvas.onwheel = function (evt) {
94 | evt.preventDefault();
95 | if (!ws) {
96 | return false;
97 | }
98 | ws.send(`{"x":${evt.deltaX},"y":${evt.deltaY}, "cmd":"Zoom"}`);
99 | return false;
100 | }
101 |
102 | canvas.oncontextmenu = function (evt) {
103 | evt.preventDefault();
104 | return false;
105 | }
106 |
107 | canvas.onmousedown = function (evt) {
108 | if (!ws) {
109 | return false;
110 | }
111 | evt.preventDefault();
112 |
113 | var rect = evt.target.getBoundingClientRect();
114 | var x = (evt.clientX - rect.left);
115 | var y = (evt.clientY - rect.top);
116 | checkMouseMoved(x, y);
117 | ws.send(`{"x":${x},"y":${y}, "cmd":"Mousedown", "val":"${evt.button}", "moved":${mouse_moved}}`);
118 | prev_x = x;
119 | prev_y = y;
120 | return false;
121 | }
122 |
123 | function checkMouseMoved(x, y) {
124 | if (prev_x && prev_y) {
125 | if (Math.abs(prev_x - x) < 1 && Math.abs(prev_y - y) < 1) {
126 | mouse_moved = false;
127 | } else {
128 | mouse_moved = true;
129 | }
130 | }
131 | }
132 |
133 | canvas.onmouseup = function (evt) {
134 | evt.preventDefault();
135 | if (!ws) {
136 | return false;
137 | }
138 | var rect = evt.target.getBoundingClientRect();
139 | var x = (evt.clientX - rect.left);
140 | var y = (evt.clientY - rect.top);
141 | checkMouseMoved(x, y);
142 | ws.send(`{"x":${x},"y":${y}, "cmd":"Mouseup", "val":"${evt.button}", "moved":${mouse_moved}, "ctrl":${evt.ctrlKey}}`);
143 |
144 | // open context menu if mouse hasn't been moved
145 | if (evt.button == 2 && !mouse_moved) {
146 | var top = evt.clientY;
147 | var left = evt.clientX;
148 | $("#context-menu").css({
149 | display: "block",
150 | top: top,
151 | left: left
152 | }).addClass("show");
153 | }
154 | return false;
155 | }
156 |
157 | this.document.onkeydown = function (e) {
158 | e.preventDefault();
159 | e = e || window.event;
160 | if (!ws) {
161 | return false;
162 | }
163 | ws.send(`{"cmd":"Keydown", "val":"${e.keyCode}"}`);
164 | return false;
165 | }
166 |
167 | this.document.onkeyup = function (e) {
168 | e.preventDefault();
169 | e = e || window.event;
170 | if (!ws) {
171 | return false;
172 | }
173 | ws.send(`{"cmd":"Keyup", "val":"${e.keyCode}"}`);
174 | return false;
175 | }
176 |
177 | document.getElementById("cmd_parallel").onclick = function (evt) {
178 | if (!ws) {
179 | return false;
180 | }
181 | ws.send(`{"cmd":"Fov", "val":"15"}`);
182 | return false;
183 | };
184 |
185 | document.getElementById("cmd_perspective").onclick = function (evt) {
186 | if (!ws) {
187 | return false;
188 | }
189 | ws.send(`{"cmd":"Fov", "val":"65"}`);
190 | return false;
191 | };
192 |
193 | document.getElementById("cmd_zoomextent").onclick = function (evt) {
194 | if (!ws) {
195 | return false;
196 | }
197 | ws.send(`{"cmd":"Zoomextent"}`);
198 | return false;
199 | };
200 |
201 | document.getElementById("cmd_focus").onclick = function (evt) {
202 | if (!ws) {
203 | return false;
204 | }
205 | ws.send(`{"cmd":"Focus"}`);
206 | return false;
207 | };
208 |
209 | document.getElementById("cmd_viewtop").onclick = function (evt) {
210 | if (!ws) {
211 | return false;
212 | }
213 | ws.send(`{"cmd":"View", "val":"top"}`);
214 | return false;
215 | };
216 |
217 | document.getElementById("cmd_viewbottom").onclick = function (evt) {
218 | if (!ws) {
219 | return false;
220 | }
221 | ws.send(`{"cmd":"View", "val":"bottom"}`);
222 | return false;
223 | };
224 |
225 | document.getElementById("cmd_viewleft").onclick = function (evt) {
226 | if (!ws) {
227 | return false;
228 | }
229 | ws.send(`{"cmd":"View", "val":"left"}`);
230 | return false;
231 | };
232 |
233 | document.getElementById("cmd_viewright").onclick = function (evt) {
234 | if (!ws) {
235 | return false;
236 | }
237 | ws.send(`{"cmd":"View", "val":"right"}`);
238 | return false;
239 | };
240 |
241 | document.getElementById("cmd_viewrear").onclick = function (evt) {
242 | if (!ws) {
243 | return false;
244 | }
245 | ws.send(`{"cmd":"View", "val":"rear"}`);
246 | return false;
247 | };
248 |
249 | document.getElementById("cmd_viewfront").onclick = function (evt) {
250 | if (!ws) {
251 | return false;
252 | }
253 | ws.send(`{"cmd":"View", "val":"front"}`);
254 | return false;
255 | };
256 |
257 | document.getElementById("cmd_unhideall").onclick = function (evt) {
258 | if (!ws) {
259 | return false;
260 | }
261 | ws.send(`{"cmd":"Unhide", "val":""}`);
262 | return false;
263 | };
264 |
265 | document.getElementById("cmd_hide").onclick = function (evt) {
266 | if (!ws) {
267 | return false;
268 | }
269 | ws.send(`{"cmd":"Hide"}`);
270 | return false;
271 | };
272 |
273 | document.getElementById("cmd_debug").onclick = function (evt) {
274 | if (!ws) {
275 | return false;
276 | }
277 | ws.send(`{"cmd":"Debugmode"}`);
278 | return false;
279 | };
280 |
281 | document.getElementById("cmd_qlow").onclick = function (evt) {
282 | if (!ws) {
283 | return false;
284 | }
285 | ws.send(`{"cmd":"Quality", "val":"2"}`);
286 | return false;
287 | };
288 |
289 | document.getElementById("cmd_qmid").onclick = function (evt) {
290 | if (!ws) {
291 | return false;
292 | }
293 | ws.send(`{"cmd":"Quality", "val":"1"}`);
294 | return false;
295 | };
296 |
297 | document.getElementById("cmd_encodepng").onclick = function (evt) {
298 | if (!ws) {
299 | return false;
300 | }
301 | ws.send(`{"cmd":"Encoder", "val":"png"}`);
302 | return false;
303 | };
304 |
305 | document.getElementById("cmd_encodejpeg").onclick = function (evt) {
306 | if (!ws) {
307 | return false;
308 | }
309 | ws.send(`{"cmd":"Encoder", "val":"jpeg"}`);
310 | return false;
311 | };
312 |
313 | document.getElementById("cmd_encodelibjpeg").onclick = function (evt) {
314 | if (!ws) {
315 | return false;
316 | }
317 | ws.send(`{"cmd":"Encoder", "val":"libjpeg"}`);
318 | return false;
319 | };
320 |
321 | document.getElementById("cmd_qhigh").onclick = function (evt) {
322 | if (!ws) {
323 | return false;
324 | }
325 | ws.send(`{"cmd":"Quality", "val":"0"}`);
326 | return false;
327 | };
328 |
329 | document.getElementById("cmd_imagesettings").onclick = function (evt) {
330 | if (!ws) {
331 | return false;
332 | }
333 | let contrast = document.getElementById("img_contrast").value
334 | let saturation = document.getElementById("img_saturation").value
335 | let brightness = document.getElementById("img_brightness").value
336 | let blur = document.getElementById("img_blur").value
337 | let pixel = document.getElementById("img_pixel").value
338 | ws.send(`{"cmd":"Imagesettings", "val":"${brightness}:${contrast}:${saturation}:${blur}:${pixel}"}`);
339 | return false;
340 | };
341 |
342 | document.getElementById("cmd_resetimagesettings").onclick = function (evt) {
343 | if (!ws) {
344 | return false;
345 | }
346 | document.getElementById("img_contrast").value = 0
347 | document.getElementById("img_saturation").value = 0
348 | document.getElementById("img_brightness").value = 0
349 | document.getElementById("img_blur").value = 0
350 | document.getElementById("img_pixel").value = 1.0
351 | ws.send(`{"cmd":"Imagesettings", "val":"0:0:0:0:1"}`);
352 | return false;
353 | };
354 |
355 | document.getElementById("cmd_imageinvert").onclick = function (evt) {
356 | if (!ws) {
357 | return false;
358 | }
359 | ws.send(`{"cmd":"Invert"}`);
360 | return false;
361 | };
362 |
363 | document.getElementById("close").onclick = function (evt) {
364 | if (!ws) {
365 | return false;
366 | }
367 | ws.send(`{"cmd":"Close"}`);
368 | ws.close();
369 | return false;
370 | };
371 |
372 |
373 | $("#context-menu").on("click", function () {
374 | $("#context-menu").removeClass("show").hide();
375 | });
376 |
377 | $("#context-menu a").on("click", function () {
378 | $(this).parent().removeClass("show").hide();
379 | });
380 | });
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/chai2010/guetzli-go v1.0.1 h1:Myj6KgKqhqh9/t2AYgshnnDbDO0IcnHrQJNwonbb0gI=
2 | github.com/chai2010/guetzli-go v1.0.1/go.mod h1:0JtpemKAV0vVS2Y4PBf54fY1RogDelWxjutjAn/7Gb4=
3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
8 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
9 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
10 | github.com/g3n/engine v0.1.0 h1:e+HR/X4awny6sVx0CikNG/KyH17nXNR3CIyqDJaAI30=
11 | github.com/g3n/engine v0.1.0/go.mod h1:gH3V0Zq2oM9UlI9Y+HGVkAGaUsrjHMC8d0Eiz2URXyI=
12 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
13 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
16 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
17 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
18 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
19 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
20 | github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU=
21 | github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
22 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
23 | github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
24 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
25 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
26 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
27 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
28 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
29 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
30 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
31 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
32 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
33 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
34 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
35 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
36 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
37 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
38 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
39 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
40 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
41 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
42 | github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
43 | github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
45 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
46 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
47 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
48 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
49 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
50 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
51 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
52 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
53 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
54 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
55 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
56 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
57 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
58 | github.com/llgcode/draw2d v0.0.0-20200110163050-b96d8208fcfc h1:v8qNcPPBCFppcuCW2lm5cTCbCqhq+nwy2JeBSez2M2c=
59 | github.com/llgcode/draw2d v0.0.0-20200110163050-b96d8208fcfc/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA=
60 | github.com/llgcode/draw2d v0.0.0-20200603164053-19660b984a28 h1:uahb8nqGTCUtkKCSdYwU8CsNfkTz4VEeOXxeM2E7VTQ=
61 | github.com/llgcode/draw2d v0.0.0-20200603164053-19660b984a28/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA=
62 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb h1:61ndUreYSlWFeCY44JxDDkngVoI7/1MVhEl98Nm0KOk=
63 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
64 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
65 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
66 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
67 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
68 | github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
69 | github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
70 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
71 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
73 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
74 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
75 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
76 | github.com/moethu/engine v0.0.0-20190918211458-57b17b524856 h1:RDE6QcYv8ClxZKTujhWlE9yNSgDfrvYBd/Lys/TwjYQ=
77 | github.com/moethu/engine v0.0.0-20190918211458-57b17b524856/go.mod h1:HPMic40GK6e/NtGfD6lTmw4JrRT3txuYd8hwfU/VAr0=
78 | github.com/moethu/engine v0.0.0-20200610122637-682e1e061a29 h1:vDWIUH1PtMkm3/LtP3U4Lp6v/MdSccSoQU9gi9wMmeU=
79 | github.com/moethu/engine v0.0.0-20200610122637-682e1e061a29/go.mod h1:HPMic40GK6e/NtGfD6lTmw4JrRT3txuYd8hwfU/VAr0=
80 | github.com/moethu/imaging v1.6.3 h1:UbGa8izSeiLXZQXgD5OHbdJ1Jy4X1NgwiWtMbqU6Xu0=
81 | github.com/moethu/imaging v1.6.3/go.mod h1:9fKXkeJZAd59XPQV77zEWk3KYBtN7C2OPLA3PPNRFAs=
82 | github.com/moethu/webg3n v0.0.0-20190914122806-b732bd71f80d h1:K2WCAgfNttr1hh4XXUFJ2pgdGPfdEZL18N03/I2UUmo=
83 | github.com/moethu/webg3n v0.0.0-20190914122806-b732bd71f80d/go.mod h1:6oSN6BL/kmld+Eqnuwg9y2BNCLgS07BGhbkuAYWry70=
84 | github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 h1:/6MoQlTdk1eAi0J9O89ypO8umkp+H7mpnSF2ggSL62Q=
85 | github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84/go.mod h1:Eeech2fhQ/E4bS8cdc3+SGABQ+weQYGyWBvZ/mNr5uY=
86 | github.com/nickalie/go-mozjpegbin v0.0.0-20170427050522-d8a58e243a3d h1:D5K+g5Jagd4FwmIGhv8rslipRyRM9KeFAhLbM4OHGSA=
87 | github.com/nickalie/go-mozjpegbin v0.0.0-20170427050522-d8a58e243a3d/go.mod h1:fXBfZIitqzWwC8zDH7nIzKVyI3I6QJtJ31pxcYirEy0=
88 | github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
89 | github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
90 | github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A=
91 | github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
92 | github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d h1:ls+7AYarUlUSetfnN/DKVNcK6W8mQWc6VblmOm4XwX0=
93 | github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d/go.mod h1:DO7ixpslN6XfbWzeNH9vkS5CF2FQUX81B85rYe9zDxU=
94 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
95 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
96 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
97 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
99 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
100 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
101 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
102 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
103 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
104 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
105 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
106 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
107 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
108 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
109 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
110 | github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
111 | github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
112 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
113 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
114 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
115 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
116 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
117 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
118 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
119 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0=
120 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
121 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
122 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
123 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
124 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
125 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
126 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
127 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
128 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
129 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
130 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
131 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
132 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
133 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
134 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
135 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
136 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
137 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
138 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
139 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
140 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
141 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
142 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
143 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
144 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
145 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
146 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
147 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
148 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
149 |
--------------------------------------------------------------------------------
/static/popper.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Federico Zivolo 2017
3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll)/.test(r+s+p)?e:n(o(e))}function r(e){var o=e&&e.offsetParent,i=o&&o.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(o.nodeName)&&'static'===t(o,'position')?r(o):o:e?e.ownerDocument.documentElement:document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=o?e:t,n=o?t:e,a=document.createRange();a.setStart(i,0),a.setEnd(n,0);var l=a.commonAncestorContainer;if(e!==l&&t!==l||i.contains(n))return p(l)?l:r(l);var f=s(e);return f.host?d(f.host,t):d(e,s(t).host)}function a(e){var t=1=o.clientWidth&&i>=o.clientHeight}),l=0i[e]&&!t.escapeWithReference&&(n=_(p[o],i[e]-('right'===e?p.width:p.height))),pe({},o,n)}};return n.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=se({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,i=t.reference,n=e.placement.split('-')[0],r=X,p=-1!==['top','bottom'].indexOf(n),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(i[s])&&(e.offsets.popper[d]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var i;if(!F(e.instance.modifiers,'arrow','keepTogether'))return e;var n=o.element;if('string'==typeof n){if(n=e.instance.popper.querySelector(n),!n)return e;}else if(!e.instance.popper.contains(n))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',g=a?'bottom':'right',u=L(n)[l];d[g]-us[g]&&(e.offsets.popper[m]+=d[m]+u-s[g]),e.offsets.popper=c(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=J(_(s[l]-u,v),0),e.arrowElement=n,e.offsets.arrow=(i={},pe(i,m,Math.round(v)),pe(i,h,''),i),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(k(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=y(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement),i=e.placement.split('-')[0],n=x(i),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case le.FLIP:p=[i,n];break;case le.CLOCKWISE:p=q(i);break;case le.COUNTERCLOCKWISE:p=q(i,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(i!==s||p.length===d+1)return e;i=e.placement.split('-')[0],n=x(i);var a=e.offsets.popper,l=e.offsets.reference,f=X,m='left'===i&&f(a.right)>f(l.left)||'right'===i&&f(a.left)f(l.top)||'bottom'===i&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===i&&h||'right'===i&&c||'top'===i&&g||'bottom'===i&&u,w=-1!==['top','bottom'].indexOf(i),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u);(m||b||y)&&(e.flipped=!0,(m||b)&&(i=p[d+1]),y&&(r=K(r)),e.placement=i+(r?'-'+r:''),e.offsets.popper=se({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=C(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],i=e.offsets,n=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return n[p?'left':'top']=r[o]-(s?n[p?'width':'height']:0),e.placement=x(t),e.offsets.popper=c(n),e}},hide:{order:800,enabled:!0,fn:function(e){if(!F(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=T(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right0?i:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(n){t(n).trigger(e.end)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var s in n)if(Object.prototype.hasOwnProperty.call(n,s)){var r=n[s],o=e[s],a=o&&i.isElement(o)?"element":(l=o,{}.toString.call(l).match(/\s([a-zA-Z]+)/)[1].toLowerCase());if(!new RegExp(r).test(a))throw new Error(t.toUpperCase()+': Option "'+s+'" provided type "'+a+'" but expected type "'+r+'".')}var l}};return e=("undefined"==typeof window||!window.QUnit)&&{end:"transitionend"},t.fn.emulateTransitionEnd=n,i.supportsTransitionEnd()&&(t.event.special[i.TRANSITION_END]={bindType:e.end,delegateType:e.end,handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}}),i}(e),L=(a="alert",h="."+(l="bs.alert"),c=(o=e).fn[a],u={CLOSE:"close"+h,CLOSED:"closed"+h,CLICK_DATA_API:"click"+h+".data-api"},f="alert",d="fade",_="show",g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.removeData(this._element,l),this._element=null},e._getRootElement=function(t){var e=P.getSelectorFromElement(t),n=!1;return e&&(n=o(e)[0]),n||(n=o(t).closest("."+f)[0]),n},e._triggerCloseEvent=function(t){var e=o.Event(u.CLOSE);return o(t).trigger(e),e},e._removeElement=function(t){var e=this;o(t).removeClass(_),P.supportsTransitionEnd()&&o(t).hasClass(d)?o(t).one(P.TRANSITION_END,function(n){return e._destroyElement(t,n)}).emulateTransitionEnd(150):this._destroyElement(t)},e._destroyElement=function(t){o(t).detach().trigger(u.CLOSED).remove()},t._jQueryInterface=function(e){return this.each(function(){var n=o(this),i=n.data(l);i||(i=new t(this),n.data(l,i)),"close"===e&&i[e](this)})},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),o(document).on(u.CLICK_DATA_API,'[data-dismiss="alert"]',g._handleDismiss(new g)),o.fn[a]=g._jQueryInterface,o.fn[a].Constructor=g,o.fn[a].noConflict=function(){return o.fn[a]=c,g._jQueryInterface},g),R=(m="button",E="."+(v="bs.button"),T=".data-api",y=(p=e).fn[m],C="active",I="btn",A="focus",b='[data-toggle^="button"]',D='[data-toggle="buttons"]',S="input",w=".active",N=".btn",O={CLICK_DATA_API:"click"+E+T,FOCUS_BLUR_DATA_API:"focus"+E+T+" blur"+E+T},k=function(){function t(t){this._element=t}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=p(this._element).closest(D)[0];if(n){var i=p(this._element).find(S)[0];if(i){if("radio"===i.type)if(i.checked&&p(this._element).hasClass(C))t=!1;else{var s=p(n).find(w)[0];s&&p(s).removeClass(C)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!p(this._element).hasClass(C),p(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!p(this._element).hasClass(C)),t&&p(this._element).toggleClass(C)},e.dispose=function(){p.removeData(this._element,v),this._element=null},t._jQueryInterface=function(e){return this.each(function(){var n=p(this).data(v);n||(n=new t(this),p(this).data(v,n)),"toggle"===e&&n[e]()})},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),p(document).on(O.CLICK_DATA_API,b,function(t){t.preventDefault();var e=t.target;p(e).hasClass(I)||(e=p(e).closest(N)),k._jQueryInterface.call(p(e),"toggle")}).on(O.FOCUS_BLUR_DATA_API,b,function(t){var e=p(t.target).closest(N)[0];p(e).toggleClass(A,/^focus(in)?$/.test(t.type))}),p.fn[m]=k._jQueryInterface,p.fn[m].Constructor=k,p.fn[m].noConflict=function(){return p.fn[m]=y,k._jQueryInterface},k),j=function(t){var e="carousel",n="bs.carousel",i="."+n,o=t.fn[e],a={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},l={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},h="next",c="prev",u="left",f="right",d={SLIDE:"slide"+i,SLID:"slid"+i,KEYDOWN:"keydown"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i,TOUCHEND:"touchend"+i,LOAD_DATA_API:"load"+i+".data-api",CLICK_DATA_API:"click"+i+".data-api"},_="carousel",g="active",p="slide",m="carousel-item-right",v="carousel-item-left",E="carousel-item-next",T="carousel-item-prev",y={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},C=function(){function o(e,n){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(n),this._element=t(e)[0],this._indicatorsElement=t(this._element).find(y.INDICATORS)[0],this._addEventListeners()}var C=o.prototype;return C.next=function(){this._isSliding||this._slide(h)},C.nextWhenVisible=function(){!document.hidden&&t(this._element).is(":visible")&&"hidden"!==t(this._element).css("visibility")&&this.next()},C.prev=function(){this._isSliding||this._slide(c)},C.pause=function(e){e||(this._isPaused=!0),t(this._element).find(y.NEXT_PREV)[0]&&P.supportsTransitionEnd()&&(P.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},C.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},C.to=function(e){var n=this;this._activeElement=t(this._element).find(y.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)t(this._element).one(d.SLID,function(){return n.to(e)});else{if(i===e)return this.pause(),void this.cycle();var s=e>i?h:c;this._slide(s,this._items[e])}},C.dispose=function(){t(this._element).off(i),t.removeData(this._element,n),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},C._getConfig=function(t){return t=r({},a,t),P.typeCheckConfig(e,t,l),t},C._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(d.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&(t(this._element).on(d.MOUSEENTER,function(t){return e.pause(t)}).on(d.MOUSELEAVE,function(t){return e.cycle(t)}),"ontouchstart"in document.documentElement&&t(this._element).on(d.TOUCHEND,function(){e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout(function(t){return e.cycle(t)},500+e._config.interval)}))},C._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},C._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(y.ITEM)),this._items.indexOf(e)},C._getItemByDirection=function(t,e){var n=t===h,i=t===c,s=this._getItemIndex(e),r=this._items.length-1;if((i&&0===s||n&&s===r)&&!this._config.wrap)return e;var o=(s+(t===c?-1:1))%this._items.length;return-1===o?this._items[this._items.length-1]:this._items[o]},C._triggerSlideEvent=function(e,n){var i=this._getItemIndex(e),s=this._getItemIndex(t(this._element).find(y.ACTIVE_ITEM)[0]),r=t.Event(d.SLIDE,{relatedTarget:e,direction:n,from:s,to:i});return t(this._element).trigger(r),r},C._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(y.ACTIVE).removeClass(g);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(g)}},C._slide=function(e,n){var i,s,r,o=this,a=t(this._element).find(y.ACTIVE_ITEM)[0],l=this._getItemIndex(a),c=n||a&&this._getItemByDirection(e,a),_=this._getItemIndex(c),C=Boolean(this._interval);if(e===h?(i=v,s=E,r=u):(i=m,s=T,r=f),c&&t(c).hasClass(g))this._isSliding=!1;else if(!this._triggerSlideEvent(c,r).isDefaultPrevented()&&a&&c){this._isSliding=!0,C&&this.pause(),this._setActiveIndicatorElement(c);var I=t.Event(d.SLID,{relatedTarget:c,direction:r,from:l,to:_});P.supportsTransitionEnd()&&t(this._element).hasClass(p)?(t(c).addClass(s),P.reflow(c),t(a).addClass(i),t(c).addClass(i),t(a).one(P.TRANSITION_END,function(){t(c).removeClass(i+" "+s).addClass(g),t(a).removeClass(g+" "+s+" "+i),o._isSliding=!1,setTimeout(function(){return t(o._element).trigger(I)},0)}).emulateTransitionEnd(600)):(t(a).removeClass(g),t(c).addClass(g),this._isSliding=!1,t(this._element).trigger(I)),C&&this.cycle()}},o._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s=r({},a,t(this).data());"object"==typeof e&&(s=r({},s,e));var l="string"==typeof e?e:s.slide;if(i||(i=new o(this,s),t(this).data(n,i)),"number"==typeof e)i.to(e);else if("string"==typeof l){if("undefined"==typeof i[l])throw new TypeError('No method named "'+l+'"');i[l]()}else s.interval&&(i.pause(),i.cycle())})},o._dataApiClickHandler=function(e){var i=P.getSelectorFromElement(this);if(i){var s=t(i)[0];if(s&&t(s).hasClass(_)){var a=r({},t(s).data(),t(this).data()),l=this.getAttribute("data-slide-to");l&&(a.interval=!1),o._jQueryInterface.call(t(s),a),l&&t(s).data(n).to(l),e.preventDefault()}}},s(o,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),o}();return t(document).on(d.CLICK_DATA_API,y.DATA_SLIDE,C._dataApiClickHandler),t(window).on(d.LOAD_DATA_API,function(){t(y.DATA_RIDE).each(function(){var e=t(this);C._jQueryInterface.call(e,e.data())})}),t.fn[e]=C._jQueryInterface,t.fn[e].Constructor=C,t.fn[e].noConflict=function(){return t.fn[e]=o,C._jQueryInterface},C}(e),H=function(t){var e="collapse",n="bs.collapse",i="."+n,o=t.fn[e],a={toggle:!0,parent:""},l={toggle:"boolean",parent:"(string|element)"},h={SHOW:"show"+i,SHOWN:"shown"+i,HIDE:"hide"+i,HIDDEN:"hidden"+i,CLICK_DATA_API:"click"+i+".data-api"},c="show",u="collapse",f="collapsing",d="collapsed",_="width",g="height",p={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},m=function(){function i(e,n){this._isTransitioning=!1,this._element=e,this._config=this._getConfig(n),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var i=t(p.DATA_TOGGLE),s=0;s0&&(this._selector=o,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var o=i.prototype;return o.toggle=function(){t(this._element).hasClass(c)?this.hide():this.show()},o.show=function(){var e,s,r=this;if(!this._isTransitioning&&!t(this._element).hasClass(c)&&(this._parent&&0===(e=t.makeArray(t(this._parent).find(p.ACTIVES).filter('[data-parent="'+this._config.parent+'"]'))).length&&(e=null),!(e&&(s=t(e).not(this._selector).data(n))&&s._isTransitioning))){var o=t.Event(h.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){e&&(i._jQueryInterface.call(t(e).not(this._selector),"hide"),s||t(e).data(n,null));var a=this._getDimension();t(this._element).removeClass(u).addClass(f),this._element.style[a]=0,this._triggerArray.length>0&&t(this._triggerArray).removeClass(d).attr("aria-expanded",!0),this.setTransitioning(!0);var l=function(){t(r._element).removeClass(f).addClass(u).addClass(c),r._element.style[a]="",r.setTransitioning(!1),t(r._element).trigger(h.SHOWN)};if(P.supportsTransitionEnd()){var _="scroll"+(a[0].toUpperCase()+a.slice(1));t(this._element).one(P.TRANSITION_END,l).emulateTransitionEnd(600),this._element.style[a]=this._element[_]+"px"}else l()}}},o.hide=function(){var e=this;if(!this._isTransitioning&&t(this._element).hasClass(c)){var n=t.Event(h.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",P.reflow(this._element),t(this._element).addClass(f).removeClass(u).removeClass(c),this._triggerArray.length>0)for(var s=0;s0&&t(n).toggleClass(d,!i).attr("aria-expanded",i)}},i._getTargetFromElement=function(e){var n=P.getSelectorFromElement(e);return n?t(n)[0]:null},i._jQueryInterface=function(e){return this.each(function(){var s=t(this),o=s.data(n),l=r({},a,s.data(),"object"==typeof e&&e);if(!o&&l.toggle&&/show|hide/.test(e)&&(l.toggle=!1),o||(o=new i(this,l),s.data(n,o)),"string"==typeof e){if("undefined"==typeof o[e])throw new TypeError('No method named "'+e+'"');o[e]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),i}();return t(document).on(h.CLICK_DATA_API,p.DATA_TOGGLE,function(e){"A"===e.currentTarget.tagName&&e.preventDefault();var i=t(this),s=P.getSelectorFromElement(this);t(s).each(function(){var e=t(this),s=e.data(n)?"toggle":i.data();m._jQueryInterface.call(e,s)})}),t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=o,m._jQueryInterface},m}(e),W=function(t){var e="dropdown",i="bs.dropdown",o="."+i,a=".data-api",l=t.fn[e],h=new RegExp("38|40|27"),c={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,CLICK:"click"+o,CLICK_DATA_API:"click"+o+a,KEYDOWN_DATA_API:"keydown"+o+a,KEYUP_DATA_API:"keyup"+o+a},u="disabled",f="show",d="dropup",_="dropright",g="dropleft",p="dropdown-menu-right",m="dropdown-menu-left",v="position-static",E='[data-toggle="dropdown"]',T=".dropdown form",y=".dropdown-menu",C=".navbar-nav",I=".dropdown-menu .dropdown-item:not(.disabled)",A="top-start",b="top-end",D="bottom-start",S="bottom-end",w="right-start",N="left-start",O={offset:0,flip:!0,boundary:"scrollParent"},k={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)"},L=function(){function a(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var l=a.prototype;return l.toggle=function(){if(!this._element.disabled&&!t(this._element).hasClass(u)){var e=a._getParentFromElement(this._element),i=t(this._menu).hasClass(f);if(a._clearMenus(),!i){var s={relatedTarget:this._element},r=t.Event(c.SHOW,s);if(t(e).trigger(r),!r.isDefaultPrevented()){if(!this._inNavbar){if("undefined"==typeof n)throw new TypeError("Bootstrap dropdown require Popper.js (https://popper.js.org)");var o=this._element;t(e).hasClass(d)&&(t(this._menu).hasClass(m)||t(this._menu).hasClass(p))&&(o=e),"scrollParent"!==this._config.boundary&&t(e).addClass(v),this._popper=new n(o,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===t(e).closest(C).length&&t("body").children().on("mouseover",null,t.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),t(this._menu).toggleClass(f),t(e).toggleClass(f).trigger(t.Event(c.SHOWN,s))}}}},l.dispose=function(){t.removeData(this._element,i),t(this._element).off(o),this._element=null,this._menu=null,null!==this._popper&&(this._popper.destroy(),this._popper=null)},l.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},l._addEventListeners=function(){var e=this;t(this._element).on(c.CLICK,function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})},l._getConfig=function(n){return n=r({},this.constructor.Default,t(this._element).data(),n),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},l._getMenuElement=function(){if(!this._menu){var e=a._getParentFromElement(this._element);this._menu=t(e).find(y)[0]}return this._menu},l._getPlacement=function(){var e=t(this._element).parent(),n=D;return e.hasClass(d)?(n=A,t(this._menu).hasClass(p)&&(n=b)):e.hasClass(_)?n=w:e.hasClass(g)?n=N:t(this._menu).hasClass(p)&&(n=S),n},l._detectNavbar=function(){return t(this._element).closest(".navbar").length>0},l._getPopperConfig=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets)||{}),e}:e.offset=this._config.offset,{placement:this._getPlacement(),modifiers:{offset:e,flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}}},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i);if(n||(n=new a(this,"object"==typeof e?e:null),t(this).data(i,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},a._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=t.makeArray(t(E)),s=0;s0&&r--,40===e.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},p._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},p._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},f="show",d="out",_={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,INSERTED:"inserted"+o,CLICK:"click"+o,FOCUSIN:"focusin"+o,FOCUSOUT:"focusout"+o,MOUSEENTER:"mouseenter"+o,MOUSELEAVE:"mouseleave"+o},g="fade",p="show",m=".tooltip-inner",v=".arrow",E="hover",T="focus",y="click",C="manual",I=function(){function a(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var I=a.prototype;return I.enable=function(){this._isEnabled=!0},I.disable=function(){this._isEnabled=!1},I.toggleEnabled=function(){this._isEnabled=!this._isEnabled},I.toggle=function(e){if(this._isEnabled)if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p))return void this._leave(null,this);this._enter(null,this)}},I.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},I.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var i=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(i);var s=t.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),o=P.getUID(this.constructor.NAME);r.setAttribute("id",o),this.element.setAttribute("aria-describedby",o),this.setContent(),this.config.animation&&t(r).addClass(g);var l="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(r).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(r).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:v},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(r).addClass(p),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d&&e._leave(null,e)};P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(this.tip).one(P.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},I.hide=function(e){var n=this,i=this.getTipElement(),s=t.Event(this.constructor.Event.HIDE),r=function(){n._hoverState!==f&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(s),s.isDefaultPrevented()||(t(i).removeClass(p),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[y]=!1,this._activeTrigger[T]=!1,this._activeTrigger[E]=!1,P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(i).one(P.TRANSITION_END,r).emulateTransitionEnd(150):r(),this._hoverState="")},I.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},I.isWithContent=function(){return Boolean(this.getTitle())},I.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},I.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},I.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(m),this.getTitle()),e.removeClass(g+" "+p)},I.setElementContent=function(e,n){var i=this.config.html;"object"==typeof n&&(n.nodeType||n.jquery)?i?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[i?"html":"text"](n)},I.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},I._getAttachment=function(t){return c[t.toUpperCase()]},I._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==C){var i=n===E?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,s=n===E?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(s,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},I._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},I._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?T:E]=!0),t(n.getTipElement()).hasClass(p)||n._hoverState===f?n._hoverState=f:(clearTimeout(n._timeout),n._hoverState=f,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===f&&n.show()},n.config.delay.show):n.show())},I._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?T:E]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d&&n.hide()},n.config.delay.hide):n.hide())},I._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},I._getConfig=function(n){return"number"==typeof(n=r({},this.constructor.Default,t(this.element).data(),n)).delay&&(n.delay={show:n.delay,hide:n.delay}),"number"==typeof n.title&&(n.title=n.title.toString()),"number"==typeof n.content&&(n.content=n.content.toString()),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},I._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},I._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},I._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},I._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(g),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i),s="object"==typeof e&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,s),t(this).data(i,n)),"string"==typeof e)){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return i}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return o}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=I._jQueryInterface,t.fn[e].Constructor=I,t.fn[e].noConflict=function(){return t.fn[e]=a,I._jQueryInterface},I}(e),x=function(t){var e="popover",n="bs.popover",i="."+n,o=t.fn[e],a=new RegExp("(^|\\s)bs-popover\\S+","g"),l=r({},U.Default,{placement:"right",trigger:"click",content:"",template:''}),h=r({},U.DefaultType,{content:"(string|element|function)"}),c="fade",u="show",f=".popover-header",d=".popover-body",_={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},g=function(r){var o,g;function p(){return r.apply(this,arguments)||this}g=r,(o=p).prototype=Object.create(g.prototype),o.prototype.constructor=o,o.__proto__=g;var m=p.prototype;return m.isWithContent=function(){return this.getTitle()||this._getContent()},m.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},m.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},m.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(f),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(d),n),e.removeClass(c+" "+u)},m._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},m._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(a);null!==n&&n.length>0&&e.removeClass(n.join(""))},p._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s="object"==typeof e?e:null;if((i||!/destroy|hide/.test(e))&&(i||(i=new p(this,s),t(this).data(n,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}})},s(p,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return l}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return n}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return h}}]),p}(U);return t.fn[e]=g._jQueryInterface,t.fn[e].Constructor=g,t.fn[e].noConflict=function(){return t.fn[e]=o,g._jQueryInterface},g}(e),K=function(t){var e="scrollspy",n="bs.scrollspy",i="."+n,o=t.fn[e],a={offset:10,method:"auto",target:""},l={offset:"number",method:"string",target:"(string|element)"},h={ACTIVATE:"activate"+i,SCROLL:"scroll"+i,LOAD_DATA_API:"load"+i+".data-api"},c="dropdown-item",u="active",f={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},d="offset",_="position",g=function(){function o(e,n){var i=this;this._element=e,this._scrollElement="BODY"===e.tagName?window:e,this._config=this._getConfig(n),this._selector=this._config.target+" "+f.NAV_LINKS+","+this._config.target+" "+f.LIST_ITEMS+","+this._config.target+" "+f.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,t(this._scrollElement).on(h.SCROLL,function(t){return i._process(t)}),this.refresh(),this._process()}var g=o.prototype;return g.refresh=function(){var e=this,n=this._scrollElement===this._scrollElement.window?d:_,i="auto"===this._config.method?n:this._config.method,s=i===_?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.makeArray(t(this._selector)).map(function(e){var n,r=P.getSelectorFromElement(e);if(r&&(n=t(r)[0]),n){var o=n.getBoundingClientRect();if(o.width||o.height)return[t(n)[i]().top+s,r]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},g.dispose=function(){t.removeData(this._element,n),t(this._scrollElement).off(i),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},g._getConfig=function(n){if("string"!=typeof(n=r({},a,n)).target){var i=t(n.target).attr("id");i||(i=P.getUID(e),t(n.target).attr("id",i)),n.target="#"+i}return P.typeCheckConfig(e,n,l),n},g._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},g._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},g._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},g._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var s=this._offsets.length;s--;){this._activeTarget!==this._targets[s]&&t>=this._offsets[s]&&("undefined"==typeof this._offsets[s+1]||t li > .active",g='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',p=".dropdown-toggle",m="> .dropdown-menu .active",v=function(){function n(t){this._element=t}var i=n.prototype;return i.show=function(){var e=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&t(this._element).hasClass(a)||t(this._element).hasClass(l))){var n,i,s=t(this._element).closest(f)[0],o=P.getSelectorFromElement(this._element);if(s){var h="UL"===s.nodeName?_:d;i=(i=t.makeArray(t(s).find(h)))[i.length-1]}var c=t.Event(r.HIDE,{relatedTarget:this._element}),u=t.Event(r.SHOW,{relatedTarget:i});if(i&&t(i).trigger(c),t(this._element).trigger(u),!u.isDefaultPrevented()&&!c.isDefaultPrevented()){o&&(n=t(o)[0]),this._activate(this._element,s);var g=function(){var n=t.Event(r.HIDDEN,{relatedTarget:e._element}),s=t.Event(r.SHOWN,{relatedTarget:i});t(i).trigger(n),t(e._element).trigger(s)};n?this._activate(n,n.parentNode,g):g()}}},i.dispose=function(){t.removeData(this._element,e),this._element=null},i._activate=function(e,n,i){var s=this,r=("UL"===n.nodeName?t(n).find(_):t(n).children(d))[0],o=i&&P.supportsTransitionEnd()&&r&&t(r).hasClass(h),a=function(){return s._transitionComplete(e,r,i)};r&&o?t(r).one(P.TRANSITION_END,a).emulateTransitionEnd(150):a()},i._transitionComplete=function(e,n,i){if(n){t(n).removeClass(c+" "+a);var s=t(n.parentNode).find(m)[0];s&&t(s).removeClass(a),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(t(e).addClass(a),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!0),P.reflow(e),t(e).addClass(c),e.parentNode&&t(e.parentNode).hasClass(o)){var r=t(e).closest(u)[0];r&&t(r).find(p).addClass(a),e.setAttribute("aria-expanded",!0)}i&&i()},n._jQueryInterface=function(i){return this.each(function(){var s=t(this),r=s.data(e);if(r||(r=new n(this),s.data(e,r)),"string"==typeof i){if("undefined"==typeof r[i])throw new TypeError('No method named "'+i+'"');r[i]()}})},s(n,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),n}();return t(document).on(r.CLICK_DATA_API,g,function(e){e.preventDefault(),v._jQueryInterface.call(t(this),"show")}),t.fn.tab=v._jQueryInterface,t.fn.tab.Constructor=v,t.fn.tab.noConflict=function(){return t.fn.tab=i,v._jQueryInterface},v}(e);!function(t){if("undefined"==typeof t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=P,t.Alert=L,t.Button=R,t.Carousel=j,t.Collapse=H,t.Dropdown=W,t.Modal=M,t.Popover=x,t.Scrollspy=K,t.Tab=V,t.Tooltip=U,Object.defineProperty(t,"__esModule",{value:!0})});
7 | //# sourceMappingURL=bootstrap.min.js.map
--------------------------------------------------------------------------------