add a prompt for using <canvas>. add the live graph demo

This commit is contained in:
sawka
2024-11-06 23:57:15 -08:00
parent efb9536b49
commit 53396aa806
4 changed files with 593 additions and 2 deletions
+258
View File
@@ -0,0 +1,258 @@
# VDOM Canvas Operations Guide
The VDOM system provides a powerful way to interact with HTML Canvas elements through the `QueueRefOp` function. This guide explains how to use canvas operations effectively in your VDOM applications.
## Basic Canvas Setup
First, create a canvas element and get a reference to it:
```go
canvasRef := vdom.UseVDomRef(ctx)
return vdom.E("canvas",
vdom.P("ref", canvasRef),
vdom.P("width", "300"),
vdom.P("height", "300"),
vdom.PStyle("width", 300),
vdom.PStyle("height", 300),
)
```
## Queuing Canvas Operations
Use `QueueRefOp` to send operations to the canvas. Each operation maps directly to a Canvas 2D context method:
```go
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fillStyle",
Params: []any{"#ff0000"},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fillRect",
Params: []any{0, 0, 100, 100},
})
```
## Reference Management
The VDOM canvas system has two key uses for references:
1. **Capturing Canvas API Return Values**
Some canvas operations return objects (like gradients) that cannot be directly serialized. Use the `Ref` field to capture these return values:
```go
// Create and capture a gradient
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "createLinearGradient",
Params: []any{0, 0, 200, 0},
Ref: "myGradient", // Captures the returned gradient object
})
// Use the gradient in another operation
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fillStyle",
Params: []any{"#ref:myGradient"}, // Reference the captured gradient
})
```
2. **Storing Complex Data**
You can also store and reuse any JSON-compatible data:
```go
// Store configuration data
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "addRef",
Ref: "pointsConfig",
Params: []any{[]float64{10, 20, 30, 40, 50, 60}},
})
// Use stored data with spreadRef
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "moveTo",
Params: []any{"#spreadRef:pointsConfig"},
})
// Clean up when done
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "dropRef",
Params: []any{"pointsConfig"},
})
```
Reference operations support:
- `#ref:id` - Use the referenced value directly (crucial for canvas-created objects)
- `#spreadRef:id` - Spread an array reference as multiple parameters
## Common Canvas Operations
Here are commonly used canvas operations:
1. **Basic Drawing**
```go
// Clear canvas
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "clearRect",
Params: []any{0, 0, width, height},
})
// Set fill color
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fillStyle",
Params: []any{"rgba(255, 0, 0, 0.5)"},
})
// Draw rectangle
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fillRect",
Params: []any{x, y, width, height},
})
```
2. **Path Operations**
```go
// Begin a new path
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
// Draw circle
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "arc",
Params: []any{x, y, radius, 0, 2 * math.Pi},
})
// Fill the path
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fill",
Params: nil,
})
```
## Animation and Updates
For smooth animations, use UseEffect with a render timestamp:
```go
renderTs := vdom.UseRenderTs(ctx)
lastRenderTs := vdom.UseRef(ctx, int64(0))
vdom.UseEffect(ctx, func() func() {
if !canvasRef.HasCurrent {
return nil
}
// Limit frame rate
if renderTs-lastRenderTs.Current < 30 {
return nil
}
lastRenderTs.Current = renderTs
// Queue drawing operations here
// Schedule next frame
go func() {
time.Sleep(60 * time.Millisecond)
AppClient.SendAsyncInitiation()
}()
return nil
}, []any{renderTs})
```
## Best Practices
1. **Performance**
- Batch related operations together
- Use `clearRect` instead of resizing the canvas
- Limit animation frame rates with render timestamps
- Clean up any animation loops in UseEffect cleanup functions
2. **Reference Management**
- Clean up references with `dropRef` when no longer needed
- Use `#spreadRef` when passing array data to operations
- Store frequently used complex data as references
3. **Error Handling**
- Check `canvasRef.HasCurrent` before operations
- Validate coordinates and dimensions
- Use proper cleanup in UseEffect returns
## Complete Example: Particle System
```go
type Particle struct {
X, Y float64
VelocityX float64
VelocityY float64
Color string
Size float64
}
type CanvasUpdaterProps struct {
CanvasRef *vdom.VDomRef
}
var CanvasUpdater = vdomclient.DefineComponent[CanvasUpdaterProps](
Client, "CanvasUpdater",
func(ctx context.Context, props CanvasUpdaterProps) any {
particles, setParticles := vdom.UseState(ctx, initParticles(10))
lastRenderTs := vdom.UseRef(ctx, int64(0))
renderTs := vdom.UseRenderTs(ctx)
vdom.UseEffect(ctx, func() func() {
if !props.CanvasRef.HasCurrent {
return nil
}
if renderTs-lastRenderTs.Current < 30 {
return nil
}
lastRenderTs.Current = renderTs
// Clear canvas
vdom.QueueRefOp(ctx, props.CanvasRef, vdom.VDomRefOperation{
Op: "clearRect",
Params: []any{0, 0, 300, 300},
})
// Draw particles
for _, p := range particles {
// Set color
vdom.QueueRefOp(ctx, props.CanvasRef, vdom.VDomRefOperation{
Op: "fillStyle",
Params: []any{p.Color},
})
// Draw circle
vdom.QueueRefOp(ctx, props.CanvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
vdom.QueueRefOp(ctx, props.CanvasRef, vdom.VDomRefOperation{
Op: "arc",
Params: []any{p.X, p.Y, p.Size, 0, 2 * math.Pi},
})
vdom.QueueRefOp(ctx, props.CanvasRef, vdom.VDomRefOperation{
Op: "fill",
Params: nil,
})
}
// Update positions for next frame
newParticles := updateParticles(particles)
setParticles(newParticles)
// Schedule next frame
go func() {
time.Sleep(60 * time.Millisecond)
Client.SendAsyncInitiation()
}()
return nil
}, []any{renderTs})
return nil
},
)
```
This guide covers the essential patterns for working with Canvas in VDOM applications. Remember that while Canvas operations are powerful, they should be used judiciously in terminal-based applications to maintain good performance.
+309
View File
@@ -0,0 +1,309 @@
package main
import (
"bufio"
"context"
_ "embed"
"fmt"
"math"
"os"
"strconv"
"strings"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
)
//go:embed style.css
var styleCSS []byte
var AppClient = vdomclient.MakeClient(vdomclient.AppOpts{
CloseOnCtrlC: true,
GlobalStyles: styleCSS,
})
type DataPoint struct {
Value float64
}
const (
canvasWidth = 800
canvasHeight = 400
padding = 40
pointRadius = 3 // Made slightly smaller
)
func calculateStats(points []DataPoint) (count int, avg float64) {
count = len(points)
if count == 0 {
return count, 0
}
sum := 0.0
for _, p := range points {
sum += p.Value
}
return count, sum / float64(count)
}
var App = vdomclient.DefineComponent(AppClient, "App",
func(ctx context.Context, _ any) any {
// State for our data points and update counter
points, _, setPointsFn := vdom.UseStateWithFn(ctx, []DataPoint{})
updateCount, _, setUpdateCountFn := vdom.UseStateWithFn(ctx, 0)
// Reference for the canvas
canvasRef := vdom.UseVDomRef(ctx)
// Reference for managing the data reading goroutine
readerState := vdom.UseRef(ctx, struct {
done chan bool
active bool
}{
done: make(chan bool),
active: false,
})
// Function to draw the graph
drawGraph := func() {
if !canvasRef.HasCurrent || len(points) == 0 {
return
}
// Clear canvas
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "clearRect",
Params: []any{0, 0, canvasWidth, canvasHeight},
})
// Find max value for scaling
maxVal := points[0].Value
for _, p := range points {
if p.Value > maxVal {
maxVal = p.Value
}
}
maxVal = maxVal * 1.05 // Add 5% buffer
// Calculate scales
xScale := float64(canvasWidth-2*padding) / math.Max(float64(len(points)-1), 1)
yScale := float64(canvasHeight-2*padding) / maxVal
// Draw grid (new!)
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "strokeStyle",
Params: []any{"#333333"},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineWidth",
Params: []any{1},
})
// Vertical grid lines
for i := 0; i < 10; i++ {
x := padding + (float64(i) * (canvasWidth - 2*padding) / 9)
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "moveTo",
Params: []any{x, padding},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineTo",
Params: []any{x, canvasHeight - padding},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "stroke",
Params: nil,
})
}
// Horizontal grid lines
for i := 0; i < 10; i++ {
y := padding + (float64(i) * (canvasHeight - 2*padding) / 9)
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "moveTo",
Params: []any{padding, y},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineTo",
Params: []any{canvasWidth - padding, y},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "stroke",
Params: nil,
})
}
// Draw axes
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "strokeStyle",
Params: []any{"#666666"},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineWidth",
Params: []any{2},
})
// Y axis
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "moveTo",
Params: []any{padding, padding},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineTo",
Params: []any{padding, canvasHeight - padding},
})
// X axis
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "moveTo",
Params: []any{padding, canvasHeight - padding},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineTo",
Params: []any{canvasWidth - padding, canvasHeight - padding},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "stroke",
Params: nil,
})
// Draw data line
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "strokeStyle",
Params: []any{"#4488ff"},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineWidth",
Params: []any{2},
})
// Draw lines connecting points
for i := 0; i < len(points); i++ {
x := padding + (float64(i) * xScale)
y := canvasHeight - padding - (points[i].Value * yScale)
if i == 0 {
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "moveTo",
Params: []any{x, y},
})
} else {
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "lineTo",
Params: []any{x, y},
})
}
}
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "stroke",
Params: nil,
})
// Draw points
for i := 0; i < len(points); i++ {
x := padding + (float64(i) * xScale)
y := canvasHeight - padding - (points[i].Value * yScale)
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "beginPath",
Params: nil,
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fillStyle",
Params: []any{"#4488ff"},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "arc",
Params: []any{x, y, pointRadius, 0, 2 * math.Pi},
})
vdom.QueueRefOp(ctx, canvasRef, vdom.VDomRefOperation{
Op: "fill",
Params: nil,
})
}
}
// Effect to start reading data
vdom.UseEffect(ctx, func() func() {
if readerState.Current.active {
return nil
}
readerState.Current.active = true
go func() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
select {
case <-readerState.Current.done:
return
default:
text := strings.TrimSpace(scanner.Text())
if value, err := strconv.ParseFloat(text, 64); err == nil {
setPointsFn(func(points []DataPoint) []DataPoint {
return append(points, DataPoint{Value: value})
})
setUpdateCountFn(func(updateCount int) int {
return updateCount + 1
})
AppClient.SendAsyncInitiation()
}
}
}
}()
return func() {
close(readerState.Current.done)
readerState.Current.active = false
readerState.Current.done = make(chan bool)
}
}, []any{})
// Effect to handle drawing
vdom.UseEffect(ctx, func() func() {
drawGraph()
return nil
}, []any{updateCount, canvasRef.HasCurrent})
// Calculate stats
count, avg := calculateStats(points)
return vdom.E("div",
vdom.Class("graph-container"),
vdom.E("h1",
vdom.Class("graph-title"),
"Live Data Graph",
),
vdom.E("canvas",
vdom.Class("graph-canvas"),
vdom.P("ref", canvasRef),
vdom.P("width", canvasWidth),
vdom.P("height", canvasHeight),
),
vdom.E("div",
vdom.Class("graph-stats"),
fmt.Sprintf("Points: %d Average: %.2f", count, avg),
),
)
},
)
func main() {
AppClient.RunMain()
}
+23
View File
@@ -0,0 +1,23 @@
.graph-container {
padding: 24px;
background: #1a1a1a;
border-radius: 8px;
color: #ffffff;
}
.graph-title {
margin: 0 0 16px 0;
font-size: 24px;
font-weight: 600;
}
.graph-canvas {
background: #000000;
border-radius: 4px;
margin-bottom: 16px;
}
.graph-stats {
font-family: monospace;
color: #888888;
}
+3 -2
View File
@@ -1,8 +1,8 @@
# VDOM System Guide
Wave Terminal includes a powerful VDOM (Virtual DOM) system that lets developers create rich HTML/React-based UI applications directly from Go code. The system translates Go components and elements into React components that are rendered within Wave Terminal's UI.
Wave Terminal includes a powerful VDOM (Virtual DOM) system that lets developers create rich HTML/React-based UI applications directly from Go code. The system translates Go components and elements into React components that are rendered within Wave Terminal's UI. It's particularly well-suited for administrative interfaces, monitoring dashboards, data visualization, configuration managers, and form-based applications where you want a graphical interface but don't need complex browser-side interactions.
This guide explains how to use the VDOM system to create interactive applications that run in Wave Terminal. While the patterns will feel familiar to React developers (components, props, hooks), the implementation is pure Go and takes advantage of Go's strengths like goroutines for async operations.
This guide explains how to use the VDOM system to create interactive applications that run in Wave Terminal. While the patterns will feel familiar to React developers (components, props, hooks), the implementation is pure Go and takes advantage of Go's strengths like goroutines for async operations. Note that complex browser-side interactions like drag-and-drop, rich text editing, or heavy JavaScript functionality are not supported - the framework is designed for straightforward, practical terminal-based applications.
You'll learn how to:
- Create and compose components
@@ -602,5 +602,6 @@ type AppOpts struct {
- Call SendAsyncInitiation() after async state updates
- Provide keys when using ForEach() with lists
- Consider cleanup functions in UseEffect() for async operations
- <script> tags are not supported
The attached todo app demonstrates all these patterns in a complete application.