diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..600b52d --- /dev/null +++ b/embed.go @@ -0,0 +1,47 @@ +package main + +import ( + _ "embed" + "fmt" + "sync" + + "gioui.org/font" + "gioui.org/font/opentype" + "gioui.org/text" +) + +//go:embed fonts/AlbertSans-Regular.ttf +var AlbertSansRegular []byte + +//go:embed fonts/AlbertSans-Medium.ttf +var AlbertSansMedium []byte + +//go:embed fonts/AlbertSans-Light.ttf +var AlbertSansLight []byte + +//go:embed fonts/AlbertSans-SemiBold.ttf +var AlbertSansSemiBold []byte + +var ( + once sync.Once + collection []text.FontFace +) + +func Collection() []text.FontFace { + once.Do(func() { + register("AlbertSans", font.Font{}, AlbertSansRegular) + register("AlbertSans", font.Font{Weight: font.Light}, AlbertSansLight) + register("AlbertSans", font.Font{Weight: font.Medium}, AlbertSansMedium) + register("AlbertSans", font.Font{Weight: font.SemiBold}, AlbertSansSemiBold) + }) + return collection +} + +func register(typeface string, fnt font.Font, ttf []byte) { + face, err := opentype.Parse(ttf) + if err != nil { + panic(fmt.Errorf("failed to parse font: %v", err)) + } + fnt.Typeface = font.Typeface(typeface) + collection = append(collection, font.FontFace{Font: fnt, Face: face}) +} diff --git a/fonts/AlbertSans-Light.ttf b/fonts/AlbertSans-Light.ttf new file mode 100644 index 0000000..83610da Binary files /dev/null and b/fonts/AlbertSans-Light.ttf differ diff --git a/fonts/AlbertSans-Medium.ttf b/fonts/AlbertSans-Medium.ttf new file mode 100644 index 0000000..cddc178 Binary files /dev/null and b/fonts/AlbertSans-Medium.ttf differ diff --git a/fonts/AlbertSans-Regular.ttf b/fonts/AlbertSans-Regular.ttf new file mode 100644 index 0000000..3abc177 Binary files /dev/null and b/fonts/AlbertSans-Regular.ttf differ diff --git a/fonts/AlbertSans-SemiBold.ttf b/fonts/AlbertSans-SemiBold.ttf new file mode 100644 index 0000000..9fbd384 Binary files /dev/null and b/fonts/AlbertSans-SemiBold.ttf differ diff --git a/help.go b/help.go new file mode 100644 index 0000000..e08d66c --- /dev/null +++ b/help.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "image" + "image/color" + "strings" + + "gioui.org/font" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget/material" +) + +type help struct { + fontType font.Typeface + lineHeight int + h1FontSize int + h2FontSize int +} + +type command map[string]string + +// ShowHelpDialog activates a dialog panel whith a list of the available key shortcuts. +func (h *Hud) ShowHelpDialog(gtx layout.Context, th *material.Theme, isActive bool) { + var ( + panelWidth unit.Dp + panelHeight unit.Dp + ) + + // show the help dialog panel only if it's not yet activated. + if !isActive { + return + } + + paint.FillShape(gtx.Ops, color.NRGBA{R: 127, G: 127, B: 127, A: 70}, + clip.UniformRRect(image.Rectangle{ + Max: image.Point{ + X: gtx.Constraints.Max.X, + Y: gtx.Constraints.Max.X, + }, + }, 0).Op(gtx.Ops)) + + centerX := gtx.Dp(unit.Dp(windowWidth / 2)) + centerY := gtx.Dp(unit.Dp(windowHeight / 2)) + + fontSize := int(unit.Sp(h.h1FontSize)) + lineHeight := int(unit.Dp(h.lineHeight)) + + switch width := windowWidth; { + case width <= windowSizeX*1.4: + panelWidth = unit.Dp(windowWidth / 2) + default: + panelWidth = unit.Dp(windowWidth / 3) + } + ph := len(h.commands) * fontSize * lineHeight + panelHeight = unit.Dp(ph) + + px := int(unit.Dp(panelWidth / 2)) + py := int(unit.Dp(panelHeight / 2)) + dx, dy := centerX-px, centerY-py + + // Limit the applicable constraints to the panel size from this point onward. + gtx.Constraints.Min.X = gtx.Dp(panelWidth) + gtx.Constraints.Max.X = gtx.Dp(panelWidth) + + // This offset will apply to the rest of the content laid out in this function. + defer op.Offset(image.Point{X: dx, Y: dy}).Push(gtx.Ops).Pop() + + layout.Flex{ + Axis: layout.Vertical, + }.Layout(gtx, + layout.Rigid(func(gtx C) D { + paint.FillShape(gtx.Ops, color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + clip.UniformRRect(image.Rectangle{ + Max: image.Point{ + X: gtx.Dp(panelWidth), + Y: gtx.Dp(panelHeight), + }, + }, gtx.Dp(5)).Op(gtx.Ops)) + + paint.FillShape(gtx.Ops, color.NRGBA{A: 127}, + clip.Stroke{ + Path: clip.Rect{Max: image.Point{ + X: gtx.Dp(panelWidth), + Y: gtx.Dp(panelHeight), + }}.Path(), + Width: 0.2, + }.Op(), + ) + + layoutOffset := unit.Dp(20) + return layout.UniformInset(layoutOffset).Layout(gtx, func(gtx C) D { + layout.Center.Layout(gtx, func(gtx C) D { + h1 := material.H2(th, "Quick help") + h1.TextSize = unit.Sp(h.h1FontSize) + h1.Font.Typeface = h.fontType + h1.Font.Weight = font.SemiBold + + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(h1.Layout), + ) + }) + colOffset := unit.Dp(200) + gtx.Constraints.Min.X = gtx.Dp(panelWidth - layoutOffset - colOffset) + + defer op.Offset(image.Point{X: 0, Y: 50}).Push(gtx.Ops).Pop() + h.list.Layout(gtx, len(h.commands), + func(gtx C, index int) D { + builder := strings.Builder{} + if cmd, ok := h.commands[index]; ok { + for key := range cmd { + builder.WriteString(fmt.Sprintf("%s\n", key)) + } + } + h2 := material.H2(th, builder.String()) + h2.TextSize = unit.Sp(h.h2FontSize) + h2.Font.Typeface = h.fontType + h2.Font.Weight = font.Weight(font.SemiBold) + + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(h2.Layout), + ) + }, + ) + defer op.Offset(image.Point{X: gtx.Dp(colOffset), Y: 0}).Push(gtx.Ops).Pop() + h.list.Layout(gtx, len(h.commands), + func(gtx C, index int) D { + builder := strings.Builder{} + if cmd, ok := h.commands[index]; ok { + for _, desc := range cmd { + builder.WriteString(fmt.Sprintf("%s\n", desc)) + } + } + h2 := material.H2(th, builder.String()) + h2.TextSize = unit.Sp(h.h2FontSize) + h2.Font.Typeface = h.fontType + h2.Font.Weight = font.Weight(font.Regular) + + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(h2.Layout), + ) + }, + ) + return layout.Dimensions{} + }) + }), + ) +} diff --git a/hud.go b/hud.go index 646bbd8..05cd654 100644 --- a/hud.go +++ b/hud.go @@ -22,7 +22,7 @@ import ( const Version = "v1.0.3" -var hudControlBtnColor = color.NRGBA{R: 0xd9, G: 0x03, B: 0x68, A: 0xff} +var HudDefaultColor = color.NRGBA{R: 0xd9, G: 0x03, B: 0x68, A: 0xff} type ( D = layout.Dimensions @@ -30,25 +30,27 @@ type ( ) type Hud struct { - hudTag struct{} - panelInit time.Time - panelWidth int - panelHeight int - winOffsetX float64 // stores the X offset on window horizontal resize - winOffsetY float64 // stores the Y offset on window vertical resize - ctrlBtn *Easing - sliders map[int]*slider - slide *Easing - reset widget.Clickable - debug widget.Bool - list layout.List - activator gesture.Click - closer gesture.Click - closeBtn int - btnSize int - controls gesture.Hover - isActive bool - showHelp bool + hudTag struct{} + panelInit time.Time + panelWidth int + panelHeight int + winOffsetX float64 // stores the X offset on window horizontal resize + winOffsetY float64 // stores the Y offset on window vertical resize + ctrlBtn *Easing + sliders map[int]*slider + commands map[int]command + slide *Easing + reset widget.Clickable + debug widget.Bool + list layout.List + activator gesture.Click + closer gesture.Click + closeBtn int + btnSize int + controls gesture.Hover + isActive bool + showHelpPanel bool + *help } type slider struct { @@ -62,7 +64,16 @@ type slider struct { // NewHud creates a new HUD used to interactively change the default settings via sliders and checkboxes. func NewHud() *Hud { - hud := Hud{sliders: make(map[int]*slider)} + hud := Hud{ + sliders: make(map[int]*slider), + commands: make(map[int]command), + help: &help{ + fontType: "AlbertSans", + lineHeight: 3, + h1FontSize: 18, + h2FontSize: 15, + }, + } sliders := []slider{ {title: "Drag force", min: 2, value: 4, max: 25}, @@ -70,11 +81,23 @@ func NewHud() *Hud { {title: "Elasticity", min: 10, value: 30, max: 50}, {title: "Tear distance", min: 5, value: 20, max: 80}, } - for idx, s := range sliders { hud.addSlider(idx, s) } + commands := []command{ + {"F1": "Toggle the quick help panel"}, + {"Space": "Redraw the cloth"}, + {"Right click": "Tear the cloth at mouse position"}, + {"Click & hold": "Increase the mouse pressure"}, + {"Scroll Up/Down": "Change the mouse focus area"}, + {"Ctrl+click": "Pin the cloth particle at mouse position"}, + } + + for idx, cmd := range commands { + hud.commands[idx] = cmd + } + slide := &Easing{duration: 600 * time.Millisecond} hover := &Easing{duration: 700 * time.Millisecond} @@ -95,7 +118,7 @@ func (h *Hud) addSlider(index int, s slider) { } // ShowControlPanel is responsible for showing or hiding the HUD control elements. -func (h *Hud) ShowControlPanel(gtx layout.Context, th *material.Theme, m *Mouse, isActive bool) { +func (h *Hud) ShowControlPanel(gtx layout.Context, th *material.Theme, isActive bool) { if h.reset.Pressed() { for _, s := range h.sliders { s.widget.Value = s.value @@ -141,7 +164,7 @@ func (h *Hud) ShowControlPanel(gtx layout.Context, th *material.Theme, m *Mouse, paint.FillShape(gtx.Ops, color.NRGBA{A: 0xff}, clip.Stroke{ Path: path.End(), - Width: float32(unit.Dp(2)), + Width: float32(unit.Dp(3)), }.Op()) } @@ -214,7 +237,7 @@ func (h *Hud) ShowControlPanel(gtx layout.Context, th *material.Theme, m *Mouse, }), layout.Rigid(func(gtx C) D { btnTheme := material.NewTheme() - btnTheme.Palette.ContrastBg = hudControlBtnColor + btnTheme.Palette.ContrastBg = HudDefaultColor return layout.UniformInset(unit.Dp(10)).Layout(gtx, material.Button(btnTheme, &h.reset, "Reset").Layout) }), ) @@ -238,7 +261,7 @@ func (h *Hud) ShowControlPanel(gtx layout.Context, th *material.Theme, m *Mouse, } // DrawCtrlBtn draws the button which activates the main HUD area with the sliders. -func (h *Hud) DrawCtrlBtn(gtx layout.Context, th *material.Theme, m *Mouse, isActive bool) { +func (h *Hud) DrawCtrlBtn(gtx layout.Context, th *material.Theme, isActive bool) { progress := h.slide.Update(gtx, isActive) pos := h.slide.Animate(progress) * float64(h.panelHeight) offset := gtx.Dp(unit.Dp(60)) @@ -309,7 +332,7 @@ func (h *Hud) DrawCtrlBtn(gtx layout.Context, th *material.Theme, m *Mouse, isAc pointer.CursorPointer.Add(gtx.Ops) h.activator.Add(gtx.Ops) - paint.ColorOp{Color: hudControlBtnColor}.Add(gtx.Ops) + paint.ColorOp{Color: HudDefaultColor}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) return layout.Dimensions{} @@ -318,55 +341,3 @@ func (h *Hud) DrawCtrlBtn(gtx layout.Context, th *material.Theme, m *Mouse, isAc ) offStack.Pop() } - -func (h *Hud) ShowHelpDialog(gtx layout.Context, th *material.Theme, m *Mouse, isActive bool) { - if !isActive { - return - } - - layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - centerX := gtx.Constraints.Max.X / 2 - centerY := gtx.Constraints.Max.Y / 2 - - dialogWidth := gtx.Constraints.Max.X / 3 - dialogHeight := gtx.Constraints.Max.Y / 3 - - px := gtx.Dp(unit.Dp(dialogWidth / 2)) - py := gtx.Dp(unit.Dp(dialogHeight / 2)) - - dx, dy := centerX-px, centerY-py - fmt.Println(dialogWidth, dialogHeight) - - // This offset will apply to the rest of the content laid out in this function. - defer op.Offset(image.Point{X: dx, Y: dy}).Push(gtx.Ops).Pop() - - paint.FillShape(gtx.Ops, color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, - clip.Rect{Max: image.Point{ - X: dx, - Y: dy, - }}.Op()) - paint.FillShape(gtx.Ops, color.NRGBA{A: 0xff}, - clip.Stroke{ - Path: clip.Rect{Max: image.Point{ - X: dx, - Y: dy, - }}.Path(), - Width: 0.2, - }.Op(), - ) - - pointer.InputOp{ - Tag: &h.hudTag, - Types: pointer.Scroll | pointer.Move | pointer.Press | pointer.Drag | pointer.Release | pointer.Leave, - }.Add(gtx.Ops) - h.controls.Add(gtx.Ops) - - pointer.CursorPointer.Add(gtx.Ops) - - return layout.Dimensions{ - Size: gtx.Constraints.Max, - } - }), - ) -} diff --git a/main.go b/main.go index e5a1553..d1cc199 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,9 @@ const ( hudTimeout = 2.5 delta = 0.022 + windowSizeX = 1280 + windowSizeY = 820 + defaultWindowWidth = 940 defaultWindowHeigth = 580 ) @@ -63,7 +66,7 @@ func main() { go func() { w := app.NewWindow( app.Title("Gio - 2D Cloth Simulation"), - app.Size(unit.Dp(windowWidth), unit.Dp(windowHeight)), + app.Size(unit.Dp(windowSizeX), unit.Dp(windowSizeY)), ) if err := loop(w); err != nil { log.Fatal(err) @@ -163,12 +166,9 @@ func loop(w *app.Window) error { cloth.Reset(startX, startY, hud) case key.NameF1: - hud.showHelp = !hud.showHelp + hud.showHelpPanel = !hud.showHelpPanel } } - if e.Name == key.NameEscape { - w.Perform(system.ActionClose) - } } } @@ -191,6 +191,13 @@ func loop(w *app.Window) error { cloth.width = windowWidth cloth.height = windowHeight + + if e.Size.X < defaultWindowWidth { + hud.showHelpPanel = false + } + if e.Size.Y < defaultWindowHeigth { + hud.showHelpPanel = false + } } fillBackground(gtx, color.NRGBA{R: 0xf2, G: 0xf2, B: 0xf2, A: 0xff}) @@ -244,6 +251,7 @@ func loop(w *app.Window) error { } mouse.setLeftButton() initTime = time.Now() + hud.showHelpPanel = false case pointer.Release: isDragging = false @@ -287,6 +295,7 @@ func loop(w *app.Window) error { } if hud.isActive { + hud.showHelpPanel = false for _, ev := range gtx.Queue.Events(&hud.hudTag) { switch ev := ev.(type) { case pointer.Event: @@ -301,9 +310,9 @@ func loop(w *app.Window) error { } } } - hud.DrawCtrlBtn(gtx, th, mouse, hud.isActive) - hud.ShowControlPanel(gtx, th, mouse, hud.isActive) - hud.ShowHelpDialog(gtx, th, mouse, hud.showHelp) + hud.DrawCtrlBtn(gtx, th, hud.isActive) + hud.ShowControlPanel(gtx, th, hud.isActive) + hud.ShowHelpDialog(gtx, th, hud.showHelpPanel) return layout.Dimensions{} }), diff --git a/particle.go b/particle.go index 8b67efc..3b61398 100644 --- a/particle.go +++ b/particle.go @@ -87,7 +87,7 @@ func (p *Particle) update(gtx layout.Context, mouse *Mouse, hud *Hud, dt float64 if p.pinX { // Recalculate the pinned particles position when the window is resized. // We need to do this only for the pinned particles, because the rest - // of the particles will just adjust themselves. + // of the particles will just adjust themselves automatically. p.x += hud.winOffsetX p.y += hud.winOffsetY return