Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

animate svg #1088

Merged
merged 1 commit into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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