Skip to content

Commit

Permalink
animate-interval
Browse files Browse the repository at this point in the history
  • Loading branch information
alixander committed Mar 27, 2023
1 parent 718daee commit f30efc5
Show file tree
Hide file tree
Showing 14 changed files with 1,625 additions and 425 deletions.
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#### Features 🚀

- `--animate-interval` can be passed as a flag to animate multi-board diagrams. See [docs](https://d2lang.com/todo). [#1088](https://github.com/terrastruct/d2/pull/1088)
- `paper` is available as a `fill-pattern` option [#1070](https://github.com/terrastruct/d2/pull/1070)

#### Improvements 🧹
Expand Down
3 changes: 3 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ Center the SVG in the containing viewbox, such as your browser screen
.It Fl -pad Ar 100
Pixels padded around the rendered diagram
.Ns .
.It Fl -animate-interval Ar 0
If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports
.Ns .
.It Fl -browser Ar true
Browser executable that watch opens. Setting to 0 opens no browser
.Ns .
Expand Down
170 changes: 110 additions & 60 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2animate"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
Expand Down Expand Up @@ -68,14 +69,18 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "The theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "the theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
if err != nil {
return err
}
padFlag, err := ms.Opts.Int64("D2_PAD", "pad", "", d2svg.DEFAULT_PADDING, "pixels padded around the rendered diagram")
if err != nil {
return err
}
animateIntervalFlag, err := ms.Opts.Int64("D2_ANIMATE_INTERVAL", "animate-interval", "", 0, "if given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports.")
if err != nil {
return err
}
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
if err != nil {
return err
Expand Down Expand Up @@ -171,6 +176,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
}
if outputPath != "-" {
outputPath = ms.AbsPath(outputPath)
if *animateIntervalFlag > 0 {
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath))
}
}
}

match := d2themescatalog.Find(*themeFlag)
Expand Down Expand Up @@ -231,24 +242,29 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
}()
}

renderOpts := d2svg.RenderOpts{
Pad: int(*padFlag),
Sketch: *sketchFlag,
Center: *centerFlag,
ThemeID: *themeFlag,
DarkThemeID: darkThemeFlag,
}

if *watchFlag {
if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
}
w, err := newWatcher(ctx, ms, watcherOpts{
layoutPlugin: plugin,
sketch: *sketchFlag,
center: *centerFlag,
themeID: *themeFlag,
darkThemeID: darkThemeFlag,
pad: *padFlag,
host: *hostFlag,
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
bundle: *bundleFlag,
forceAppendix: *forceAppendixFlag,
pw: pw,
layoutPlugin: plugin,
renderOpts: renderOpts,
animateInterval: *animateIntervalFlag,
host: *hostFlag,
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
bundle: *bundleFlag,
forceAppendix: *forceAppendixFlag,
pw: pw,
})
if err != nil {
return err
Expand All @@ -259,7 +275,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
defer cancel()

_, written, err := compile(ctx, ms, plugin, *sketchFlag, *centerFlag, *padFlag, *themeFlag, darkThemeFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
_, written, err := compile(ctx, ms, plugin, renderOpts, *animateIntervalFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
if err != nil {
if written {
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
Expand All @@ -269,7 +285,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
return nil
}

func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, renderOpts d2svg.RenderOpts, animateInterval int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
start := time.Now()
input, err := ms.ReadPath(inputPath)
if err != nil {
Expand All @@ -285,9 +301,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
opts := &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,
ThemeID: themeID,
ThemeID: renderOpts.ThemeID,
}
if sketch {
if renderOpts.Sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
}

Expand All @@ -302,6 +318,14 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
}
cancel()

if animateInterval > 0 {
masterID, err := diagram.HashID()
if err != nil {
return nil, false, err
}
renderOpts.MasterID = masterID
}

pluginInfo, err := plugin.Info(ctx)
if err != nil {
return nil, false, err
Expand All @@ -312,27 +336,42 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
return nil, false, err
}

var svg []byte
if filepath.Ext(outputPath) == ".pdf" {
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
svg, err = renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, outputPath, page, ruler, diagram, nil, nil, pageMap)
} else {
compileDur := time.Since(start)
svg, err = render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
}
if err != nil {
return svg, false, err
}

if filepath.Ext(outputPath) == ".pdf" {
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
if err != nil {
return pdf, false, err
}
dur := time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
return pdf, true, nil
} else {
compileDur := time.Since(start)
boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return nil, false, err
}
out := boards[0]
if animateInterval > 0 {
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
if err != nil {
return nil, false, err
}
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return nil, false, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return nil, false, err
}
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
}
return out, true, nil
}

return svg, true, nil
}

func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, sketch, center bool, pad int64, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
if diagram.Name != "" {
ext := filepath.Ext(outputPath)
outputPath = strings.TrimSuffix(outputPath, ext)
Expand All @@ -343,6 +382,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
boardOutputPath := outputPath
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
if outputPath == "-" {
// TODO it can if composed into one
return nil, fmt.Errorf("multiboard output cannot be written to stdout")
}
// Boards with subboards must be self-contained folders.
Expand Down Expand Up @@ -375,47 +415,55 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
stepsOutputPath += ext
}

var boards [][]byte
for _, dl := range diagram.Layers {
_, err := render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Scenarios {
_, err := render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Steps {
_, err := render(ctx, ms, compileDur, plugin, sketch, center, pad, themeID, darkThemeID, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}

if !diagram.IsFolderOnly {
start := time.Now()
svg, err := _render(ctx, ms, plugin, sketch, center, pad, themeID, darkThemeID, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return svg, err
return boards, err
}
dur := compileDur + time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
return svg, nil
if opts.MasterID == "" {
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
}
boards = append([][]byte{out}, boards...)
return boards, nil
}

return nil, nil
}

func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad int64, themeID int64, darkThemeID *int64, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := filepath.Ext(outputPath) == ".png"
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: int(pad),
Sketch: sketch,
Center: center,
ThemeID: themeID,
DarkThemeID: darkThemeID,
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
ThemeID: opts.ThemeID,
DarkThemeID: opts.DarkThemeID,
MasterID: opts.MasterID,
SetDimensions: toPNG,
})
if err != nil {
Expand Down Expand Up @@ -461,21 +509,23 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
}
}

err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return svg, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return svg, err
if opts.MasterID == "" {
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return svg, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return svg, err
}
}
if bundleErr != nil {
return svg, bundleErr
}
return svg, nil
}

func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad, themeID int64, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
var isRoot bool
if pdf == nil {
pdf = pdflib.Init()
Expand All @@ -501,9 +551,9 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
diagram.Root.Fill = "transparent"

svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: int(pad),
Sketch: sketch,
Center: center,
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
SetDimensions: true,
})
if err != nil {
Expand Down Expand Up @@ -537,26 +587,26 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
if err != nil {
return svg, err
}
err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill, diagram.Shapes, pad, viewboxX, viewboxY, pageMap)
err = pdf.AddPDFPage(pngImg, currBoardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap)
if err != nil {
return svg, err
}
}

for _, dl := range diagram.Layers {
_, err := renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Scenarios {
_, err := renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Steps {
_, err := renderPDF(ctx, ms, plugin, sketch, center, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit f30efc5

Please sign in to comment.