From f30efc5f628e6bfe7c6c464e31346f3e3701469c Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Thu, 23 Mar 2023 13:37:28 -0700 Subject: [PATCH] animate-interval --- ci/release/changelogs/next.md | 1 + ci/release/template/man/d2.1 | 3 + d2cli/main.go | 170 ++-- d2cli/watch.go | 28 +- d2renderers/d2animate/d2animate.go | 116 +++ d2renderers/d2svg/d2svg.go | 107 ++- d2target/d2target.go | 91 +- e2etests-cli/main_test.go | 36 + .../testdata/TestCLI_E2E/animation.exp.svg | 890 ++++++++++++++++++ .../TestCLI_E2E/internal_linked_pdf.exp.pdf | Bin 81564 -> 81564 bytes .../TestCLI_E2E/multiboard/life/index.exp.svg | 152 +-- .../multiboard/life_index_d2/index.exp.svg | 152 +-- .../complex-layers/dagre/sketch.exp.svg | 152 +-- .../stable/complex-layers/elk/sketch.exp.svg | 152 +-- 14 files changed, 1625 insertions(+), 425 deletions(-) create mode 100644 d2renderers/d2animate/d2animate.go create mode 100644 e2etests-cli/testdata/TestCLI_E2E/animation.exp.svg diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 17c909a710..4c027f5706 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -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 ๐Ÿงน diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1 index 6dce8cf365..f3c0d63036 100644 --- a/ci/release/template/man/d2.1 +++ b/ci/release/template/man/d2.1 @@ -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 . diff --git a/d2cli/main.go b/d2cli/main.go index dedba22b09..b10e7f64e4 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -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" @@ -68,7 +69,7 @@ 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 } @@ -76,6 +77,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { 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 @@ -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) @@ -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 @@ -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) @@ -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 { @@ -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) } @@ -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 @@ -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) @@ -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. @@ -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 { @@ -461,13 +509,15 @@ 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 @@ -475,7 +525,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc 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() @@ -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 { @@ -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 } diff --git a/d2cli/watch.go b/d2cli/watch.go index f9cfb9e81b..8f3e3c398d 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -26,6 +26,7 @@ import ( "oss.terrastruct.com/util-go/xmain" "oss.terrastruct.com/d2/d2plugin" + "oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/lib/png" ) @@ -39,20 +40,17 @@ var devMode = false var staticFS embed.FS type watcherOpts struct { - layoutPlugin d2plugin.Plugin - themeID int64 - darkThemeID *int64 - pad int64 - sketch bool - center bool - host string - port string - inputPath string - outputPath string - pwd string - bundle bool - forceAppendix bool - pw png.Playwright + layoutPlugin d2plugin.Plugin + renderOpts d2svg.RenderOpts + animateInterval int64 + host string + port string + inputPath string + outputPath string + pwd string + bundle bool + forceAppendix bool + pw png.Playwright } type watcher struct { @@ -360,7 +358,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { w.pw = newPW } - svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.center, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page) + svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.renderOpts, w.animateInterval, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page) errs := "" if err != nil { if len(svg) > 0 { diff --git a/d2renderers/d2animate/d2animate.go b/d2renderers/d2animate/d2animate.go new file mode 100644 index 0000000000..9f66476fce --- /dev/null +++ b/d2renderers/d2animate/d2animate.go @@ -0,0 +1,116 @@ +package d2animate + +import ( + "bytes" + "fmt" + "math" + "strings" + + "oss.terrastruct.com/d2/d2renderers/d2sketch" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/lib/version" +) + +var transitionDurationMS = 1 + +func makeKeyframe(delayMS, durationMS, totalMS, identifier int) string { + percentageBefore := (math.Max(0, float64(delayMS-transitionDurationMS)) / float64(totalMS)) * 100. + percentageStart := (float64(delayMS) / float64(totalMS)) * 100. + percentageEnd := (float64(delayMS+durationMS-transitionDurationMS) / float64(totalMS)) * 100. + if int(math.Ceil(percentageEnd)) == 100 { + return fmt.Sprintf(`@keyframes d2Transition-%d { + 0%%, %f%% { + opacity: 0; + } + %f%%, %f%% { + opacity: 1; + } +}`, identifier, percentageBefore, percentageStart, math.Ceil(percentageEnd)) + } + + percentageAfter := (float64(delayMS+durationMS) / float64(totalMS)) * 100. + return fmt.Sprintf(`@keyframes d2Transition-%d { + 0%%, %f%% { + opacity: 0; + } + %f%%, %f%% { + opacity: 1; + } + %f%%, 100%% { + opacity: 0; + } +}`, identifier, percentageBefore, percentageStart, percentageEnd, percentageAfter) +} + +func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderOpts, intervalMS int) ([]byte, error) { + buf := &bytes.Buffer{} + + // TODO account for stroke width of root border + + tl, br := rootDiagram.NestedBoundingBox() + left := tl.X - renderOpts.Pad + top := tl.Y - renderOpts.Pad + width := br.X - tl.X + renderOpts.Pad*2 + height := br.Y - tl.Y + renderOpts.Pad*2 + + fitToScreenWrapperOpening := fmt.Sprintf(``, + version.Version, + width, height, + ) + fmt.Fprint(buf, fitToScreenWrapperOpening) + + innerOpening := fmt.Sprintf(``, + width, height, left, top, width, height) + fmt.Fprint(buf, innerOpening) + + svgsStr := "" + for _, svg := range svgs { + svgsStr += string(svg) + " " + } + + diagramHash, err := rootDiagram.HashID() + if err != nil { + return nil, err + } + + d2svg.EmbedFonts(buf, diagramHash, svgsStr, rootDiagram.FontFamily) + + themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID) + if err != nil { + return nil, err + } + fmt.Fprintf(buf, ``, d2svg.BaseStylesheet, themeStylesheet) + + if rootDiagram.HasShape(func(s d2target.Shape) bool { + return s.Label != "" && s.Type == d2target.ShapeText + }) { + css := d2svg.MarkdownCSS + css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash)) + css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash)) + css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash)) + css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash)) + fmt.Fprintf(buf, ``, css) + } + + if renderOpts.Sketch { + d2sketch.DefineFillPatterns(buf) + } + + fmt.Fprint(buf, ``) + + for i, svg := range svgs { + str := string(svg) + str = strings.Replace(str, "") + fmt.Fprint(buf, "") + + return buf.Bytes(), nil +} diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index f7b9b915fb..33495ff4c8 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -54,10 +54,10 @@ var TooltipIcon string var LinkIcon string //go:embed style.css -var baseStylesheet string +var BaseStylesheet string //go:embed github-markdown.css -var mdCSS string +var MarkdownCSS string //go:embed dots.txt var dots string @@ -79,6 +79,10 @@ type RenderOpts struct { DarkThemeID *int64 // disables the fit to screen behavior and ensures the exported svg has the exact dimensions SetDimensions bool + + // MasterID is passed when the diagram should use something other than its own hash for unique targeting + // Currently, that's when multi-boards are collapsed + MasterID string } func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) { @@ -1382,7 +1386,7 @@ func RenderText(text string, x, height float64) string { return strings.Join(rendered, "") } -func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily) { +func EmbedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily) { fmt.Fprint(buf, ``, baseStylesheet, themeStylesheet) + if opts.MasterID == "" { + EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf` + themeStylesheet, err := ThemeCSS(diagramHash, themeID, darkThemeID) + if err != nil { + return nil, err + } + fmt.Fprintf(upperBuf, ``, BaseStylesheet, themeStylesheet) - hasMarkdown := false - for _, s := range diagram.Shapes { - if s.Label != "" && s.Type == d2target.ShapeText { - hasMarkdown = true - break + hasMarkdown := false + for _, s := range diagram.Shapes { + if s.Label != "" && s.Type == d2target.ShapeText { + hasMarkdown = true + break + } + } + if hasMarkdown { + css := MarkdownCSS + css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash)) + css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash)) + css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash)) + css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash)) + fmt.Fprintf(upperBuf, ``, css) } - } - if hasMarkdown { - css := mdCSS - css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash)) - css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash)) - css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash)) - css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash)) - fmt.Fprintf(upperBuf, ``, css) - } - if sketchRunner != nil { - d2sketch.DefineFillPatterns(upperBuf) + if sketchRunner != nil { + d2sketch.DefineFillPatterns(upperBuf) + } } // This shift is for background el to envelop the diagram @@ -1838,30 +1846,45 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { if opts.Center { alignment = "xMidYMid" } - fitToScreenWrapper := fmt.Sprintf(``, - `xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`, - version.Version, - alignment, - w, h, - dimensions, - ) + fitToScreenWrapperOpening := "" + xmlTag := "" + fitToScreenWrapperClosing := "" + idAttr := "" + tag := "g" + // Many things change when this is rendering for animation + if opts.MasterID == "" { + fitToScreenWrapperOpening = fmt.Sprintf(``, + version.Version, + alignment, + w, h, + dimensions, + ) + xmlTag = `` + fitToScreenWrapperClosing = "" + idAttr = `id="d2-svg"` + tag = "svg" + } // TODO minify - docRendered := fmt.Sprintf(`%s%s%s%s%s%s`, - ``, - fitToScreenWrapper, + docRendered := fmt.Sprintf(`%s%s<%s %s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s%s`, + xmlTag, + fitToScreenWrapperOpening, + tag, + idAttr, diagramHash, w, h, left, top, w, h, doubleBorderElStr, backgroundEl.Render(), upperBuf.String(), buf.String(), + tag, + fitToScreenWrapperClosing, ) return []byte(docRendered), nil } // TODO include only colors that are being used to reduce size -func themeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) { +func ThemeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) { out, err := singleThemeRulesets(diagramHash, themeID) if err != nil { return "", err diff --git a/d2target/d2target.go b/d2target/d2target.go index a7ad74852d..203c2ca092 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -51,18 +51,101 @@ type Diagram struct { Steps []*Diagram `json:"steps,omitempty"` } -func (diagram Diagram) HashID() (string, error) { +func (diagram Diagram) Bytes() ([]byte, error) { b1, err := json.Marshal(diagram.Shapes) if err != nil { - return "", err + return nil, err } b2, err := json.Marshal(diagram.Connections) + if err != nil { + return nil, err + } + base := append(b1, b2...) + + for _, d := range diagram.Layers { + slices, err := d.Bytes() + if err != nil { + return nil, err + } + base = append(base, slices...) + } + for _, d := range diagram.Scenarios { + slices, err := d.Bytes() + if err != nil { + return nil, err + } + base = append(base, slices...) + } + for _, d := range diagram.Steps { + slices, err := d.Bytes() + if err != nil { + return nil, err + } + base = append(base, slices...) + } + + return base, nil +} + +func (diagram Diagram) HasShape(condition func(Shape) bool) bool { + for _, d := range diagram.Layers { + if d.HasShape(condition) { + return true + } + } + for _, d := range diagram.Scenarios { + if d.HasShape(condition) { + return true + } + } + for _, d := range diagram.Steps { + if d.HasShape(condition) { + return true + } + } + for _, s := range diagram.Shapes { + if condition(s) { + return true + } + } + return false +} + +func (diagram Diagram) HashID() (string, error) { + bytes, err := diagram.Bytes() if err != nil { return "", err } h := fnv.New32a() - h.Write(append(b1, b2...)) - return fmt.Sprint(h.Sum32()), nil + h.Write(bytes) + // CSS names can't start with numbers, so prepend a little something + return fmt.Sprintf("d2-%d", h.Sum32()), nil +} + +func (diagram Diagram) NestedBoundingBox() (topLeft, bottomRight Point) { + tl, br := diagram.BoundingBox() + for _, d := range diagram.Layers { + tl2, br2 := d.NestedBoundingBox() + tl.X = go2.Min(tl.X, tl2.X) + tl.Y = go2.Min(tl.Y, tl2.Y) + br.X = go2.Max(br.X, br2.X) + br.Y = go2.Max(br.Y, br2.Y) + } + for _, d := range diagram.Scenarios { + tl2, br2 := d.NestedBoundingBox() + tl.X = go2.Min(tl.X, tl2.X) + tl.Y = go2.Min(tl.Y, tl2.Y) + br.X = go2.Max(br.X, br2.X) + br.Y = go2.Max(br.Y, br2.Y) + } + for _, d := range diagram.Steps { + tl2, br2 := d.NestedBoundingBox() + tl.X = go2.Min(tl.X, tl2.X) + tl.Y = go2.Min(tl.Y, tl2.Y) + br.X = go2.Max(br.X, br2.X) + br.Y = go2.Max(br.Y, br2.Y) + } + return tl, br } func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) { diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index 2fdc0896d1..3a2edc3ce7 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -55,6 +55,42 @@ func TestCLI_E2E(t *testing.T) { assert.Testdata(t, ".svg", svg) }, }, + { + name: "animation", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "animation.d2", `Chicken's plan: { + style.font-size: 35 + near: top-center + shape: text +} + +steps: { + 1: { + Approach road + } + 2: { + Approach road -> Cross road + } + 3: { + Cross road -> Make you wonder why + } +} +`) + err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2") + assert.Success(t, err) + svg := readFile(t, dir, "animation.svg") + assert.Testdata(t, ".svg", svg) + }, + }, + { + name: "incompatible-animation", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "x.d2", `x -> y`) + err := runTestMain(t, ctx, dir, env, "--animate-interval=2", "x.d2", "x.png") + assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: bad usage: -animate-interval can only be used when exporting to SVG. +You provided: .png`) + }, + }, { name: "hello_world_png_sketch", skipCI: true, diff --git a/e2etests-cli/testdata/TestCLI_E2E/animation.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/animation.exp.svg new file mode 100644 index 0000000000..26e0bd29f7 --- /dev/null +++ b/e2etests-cli/testdata/TestCLI_E2E/animation.exp.svg @@ -0,0 +1,890 @@ +Chicken's plan + + +Approach roadChicken's plan + + +Approach roadCross roadChicken's plan + + +Approach roadCross roadMake you wonder whyChicken's plan + + + \ No newline at end of file diff --git a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf index 9186410df2ba553d1d9ba666ff8f7750512c16ea..6ec9eb9c95fc02539ed06896091cdd80132f02b8 100644 GIT binary patch delta 68 zcmbR9mu1dhmWC~i8CKJ0S}__-PqSh)oPN@Z@v5?^p_!?rv85)LzHfetOJYf?f`*Hg Tk%5u1fw2)(Zu>!N#@kE)C*u`J delta 68 zcmbR9mu1dhmWC~i8CKJ0SuyHQPqSh)ntsxX@v5?sp@o5&v6&{9zHfetOJYf?f`*Hg Tk%5u1fw2)(Zu>!N#@kE)CEyh? diff --git a/e2etests-cli/testdata/TestCLI_E2E/multiboard/life/index.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/multiboard/life/index.exp.svg index 969fa5c88e..bc4758b7ef 100644 --- a/e2etests-cli/testdata/TestCLI_E2E/multiboard/life/index.exp.svg +++ b/e2etests-cli/testdata/TestCLI_E2E/multiboard/life/index.exp.svg @@ -1,9 +1,9 @@ -xy + .d2-524372316 .fill-N1{fill:#0A0F25;} + .d2-524372316 .fill-N2{fill:#676C7E;} + .d2-524372316 .fill-N3{fill:#9499AB;} + .d2-524372316 .fill-N4{fill:#CFD2DD;} + .d2-524372316 .fill-N5{fill:#DEE1EB;} + .d2-524372316 .fill-N6{fill:#EEF1F8;} + .d2-524372316 .fill-N7{fill:#FFFFFF;} + .d2-524372316 .fill-B1{fill:#0D32B2;} + .d2-524372316 .fill-B2{fill:#0D32B2;} + .d2-524372316 .fill-B3{fill:#E3E9FD;} + .d2-524372316 .fill-B4{fill:#E3E9FD;} + .d2-524372316 .fill-B5{fill:#EDF0FD;} + .d2-524372316 .fill-B6{fill:#F7F8FE;} + .d2-524372316 .fill-AA2{fill:#4A6FF3;} + .d2-524372316 .fill-AA4{fill:#EDF0FD;} + .d2-524372316 .fill-AA5{fill:#F7F8FE;} + .d2-524372316 .fill-AB4{fill:#EDF0FD;} + .d2-524372316 .fill-AB5{fill:#F7F8FE;} + .d2-524372316 .stroke-N1{stroke:#0A0F25;} + .d2-524372316 .stroke-N2{stroke:#676C7E;} + .d2-524372316 .stroke-N3{stroke:#9499AB;} + .d2-524372316 .stroke-N4{stroke:#CFD2DD;} + .d2-524372316 .stroke-N5{stroke:#DEE1EB;} + .d2-524372316 .stroke-N6{stroke:#EEF1F8;} + .d2-524372316 .stroke-N7{stroke:#FFFFFF;} + .d2-524372316 .stroke-B1{stroke:#0D32B2;} + .d2-524372316 .stroke-B2{stroke:#0D32B2;} + .d2-524372316 .stroke-B3{stroke:#E3E9FD;} + .d2-524372316 .stroke-B4{stroke:#E3E9FD;} + .d2-524372316 .stroke-B5{stroke:#EDF0FD;} + .d2-524372316 .stroke-B6{stroke:#F7F8FE;} + .d2-524372316 .stroke-AA2{stroke:#4A6FF3;} + .d2-524372316 .stroke-AA4{stroke:#EDF0FD;} + .d2-524372316 .stroke-AA5{stroke:#F7F8FE;} + .d2-524372316 .stroke-AB4{stroke:#EDF0FD;} + .d2-524372316 .stroke-AB5{stroke:#F7F8FE;} + .d2-524372316 .background-color-N1{background-color:#0A0F25;} + .d2-524372316 .background-color-N2{background-color:#676C7E;} + .d2-524372316 .background-color-N3{background-color:#9499AB;} + .d2-524372316 .background-color-N4{background-color:#CFD2DD;} + .d2-524372316 .background-color-N5{background-color:#DEE1EB;} + .d2-524372316 .background-color-N6{background-color:#EEF1F8;} + .d2-524372316 .background-color-N7{background-color:#FFFFFF;} + .d2-524372316 .background-color-B1{background-color:#0D32B2;} + .d2-524372316 .background-color-B2{background-color:#0D32B2;} + .d2-524372316 .background-color-B3{background-color:#E3E9FD;} + .d2-524372316 .background-color-B4{background-color:#E3E9FD;} + .d2-524372316 .background-color-B5{background-color:#EDF0FD;} + .d2-524372316 .background-color-B6{background-color:#F7F8FE;} + .d2-524372316 .background-color-AA2{background-color:#4A6FF3;} + .d2-524372316 .background-color-AA4{background-color:#EDF0FD;} + .d2-524372316 .background-color-AA5{background-color:#F7F8FE;} + .d2-524372316 .background-color-AB4{background-color:#EDF0FD;} + .d2-524372316 .background-color-AB5{background-color:#F7F8FE;} + .d2-524372316 .color-N1{color:#0A0F25;} + .d2-524372316 .color-N2{color:#676C7E;} + .d2-524372316 .color-N3{color:#9499AB;} + .d2-524372316 .color-N4{color:#CFD2DD;} + .d2-524372316 .color-N5{color:#DEE1EB;} + .d2-524372316 .color-N6{color:#EEF1F8;} + .d2-524372316 .color-N7{color:#FFFFFF;} + .d2-524372316 .color-B1{color:#0D32B2;} + .d2-524372316 .color-B2{color:#0D32B2;} + .d2-524372316 .color-B3{color:#E3E9FD;} + .d2-524372316 .color-B4{color:#E3E9FD;} + .d2-524372316 .color-B5{color:#EDF0FD;} + .d2-524372316 .color-B6{color:#F7F8FE;} + .d2-524372316 .color-AA2{color:#4A6FF3;} + .d2-524372316 .color-AA4{color:#EDF0FD;} + .d2-524372316 .color-AA5{color:#F7F8FE;} + .d2-524372316 .color-AB4{color:#EDF0FD;} + .d2-524372316 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xy diff --git a/e2etests-cli/testdata/TestCLI_E2E/multiboard/life_index_d2/index.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/multiboard/life_index_d2/index.exp.svg index 969fa5c88e..bc4758b7ef 100644 --- a/e2etests-cli/testdata/TestCLI_E2E/multiboard/life_index_d2/index.exp.svg +++ b/e2etests-cli/testdata/TestCLI_E2E/multiboard/life_index_d2/index.exp.svg @@ -1,9 +1,9 @@ -xy + .d2-524372316 .fill-N1{fill:#0A0F25;} + .d2-524372316 .fill-N2{fill:#676C7E;} + .d2-524372316 .fill-N3{fill:#9499AB;} + .d2-524372316 .fill-N4{fill:#CFD2DD;} + .d2-524372316 .fill-N5{fill:#DEE1EB;} + .d2-524372316 .fill-N6{fill:#EEF1F8;} + .d2-524372316 .fill-N7{fill:#FFFFFF;} + .d2-524372316 .fill-B1{fill:#0D32B2;} + .d2-524372316 .fill-B2{fill:#0D32B2;} + .d2-524372316 .fill-B3{fill:#E3E9FD;} + .d2-524372316 .fill-B4{fill:#E3E9FD;} + .d2-524372316 .fill-B5{fill:#EDF0FD;} + .d2-524372316 .fill-B6{fill:#F7F8FE;} + .d2-524372316 .fill-AA2{fill:#4A6FF3;} + .d2-524372316 .fill-AA4{fill:#EDF0FD;} + .d2-524372316 .fill-AA5{fill:#F7F8FE;} + .d2-524372316 .fill-AB4{fill:#EDF0FD;} + .d2-524372316 .fill-AB5{fill:#F7F8FE;} + .d2-524372316 .stroke-N1{stroke:#0A0F25;} + .d2-524372316 .stroke-N2{stroke:#676C7E;} + .d2-524372316 .stroke-N3{stroke:#9499AB;} + .d2-524372316 .stroke-N4{stroke:#CFD2DD;} + .d2-524372316 .stroke-N5{stroke:#DEE1EB;} + .d2-524372316 .stroke-N6{stroke:#EEF1F8;} + .d2-524372316 .stroke-N7{stroke:#FFFFFF;} + .d2-524372316 .stroke-B1{stroke:#0D32B2;} + .d2-524372316 .stroke-B2{stroke:#0D32B2;} + .d2-524372316 .stroke-B3{stroke:#E3E9FD;} + .d2-524372316 .stroke-B4{stroke:#E3E9FD;} + .d2-524372316 .stroke-B5{stroke:#EDF0FD;} + .d2-524372316 .stroke-B6{stroke:#F7F8FE;} + .d2-524372316 .stroke-AA2{stroke:#4A6FF3;} + .d2-524372316 .stroke-AA4{stroke:#EDF0FD;} + .d2-524372316 .stroke-AA5{stroke:#F7F8FE;} + .d2-524372316 .stroke-AB4{stroke:#EDF0FD;} + .d2-524372316 .stroke-AB5{stroke:#F7F8FE;} + .d2-524372316 .background-color-N1{background-color:#0A0F25;} + .d2-524372316 .background-color-N2{background-color:#676C7E;} + .d2-524372316 .background-color-N3{background-color:#9499AB;} + .d2-524372316 .background-color-N4{background-color:#CFD2DD;} + .d2-524372316 .background-color-N5{background-color:#DEE1EB;} + .d2-524372316 .background-color-N6{background-color:#EEF1F8;} + .d2-524372316 .background-color-N7{background-color:#FFFFFF;} + .d2-524372316 .background-color-B1{background-color:#0D32B2;} + .d2-524372316 .background-color-B2{background-color:#0D32B2;} + .d2-524372316 .background-color-B3{background-color:#E3E9FD;} + .d2-524372316 .background-color-B4{background-color:#E3E9FD;} + .d2-524372316 .background-color-B5{background-color:#EDF0FD;} + .d2-524372316 .background-color-B6{background-color:#F7F8FE;} + .d2-524372316 .background-color-AA2{background-color:#4A6FF3;} + .d2-524372316 .background-color-AA4{background-color:#EDF0FD;} + .d2-524372316 .background-color-AA5{background-color:#F7F8FE;} + .d2-524372316 .background-color-AB4{background-color:#EDF0FD;} + .d2-524372316 .background-color-AB5{background-color:#F7F8FE;} + .d2-524372316 .color-N1{color:#0A0F25;} + .d2-524372316 .color-N2{color:#676C7E;} + .d2-524372316 .color-N3{color:#9499AB;} + .d2-524372316 .color-N4{color:#CFD2DD;} + .d2-524372316 .color-N5{color:#DEE1EB;} + .d2-524372316 .color-N6{color:#EEF1F8;} + .d2-524372316 .color-N7{color:#FFFFFF;} + .d2-524372316 .color-B1{color:#0D32B2;} + .d2-524372316 .color-B2{color:#0D32B2;} + .d2-524372316 .color-B3{color:#E3E9FD;} + .d2-524372316 .color-B4{color:#E3E9FD;} + .d2-524372316 .color-B5{color:#EDF0FD;} + .d2-524372316 .color-B6{color:#F7F8FE;} + .d2-524372316 .color-AA2{color:#4A6FF3;} + .d2-524372316 .color-AA4{color:#EDF0FD;} + .d2-524372316 .color-AA5{color:#F7F8FE;} + .d2-524372316 .color-AB4{color:#EDF0FD;} + .d2-524372316 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xy diff --git a/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg b/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg index 6db541f92c..b32e4dffbd 100644 --- a/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg @@ -1,9 +1,9 @@ -windowroofgarage + .d2-3917036827 .fill-N1{fill:#0A0F25;} + .d2-3917036827 .fill-N2{fill:#676C7E;} + .d2-3917036827 .fill-N3{fill:#9499AB;} + .d2-3917036827 .fill-N4{fill:#CFD2DD;} + .d2-3917036827 .fill-N5{fill:#DEE1EB;} + .d2-3917036827 .fill-N6{fill:#EEF1F8;} + .d2-3917036827 .fill-N7{fill:#FFFFFF;} + .d2-3917036827 .fill-B1{fill:#0D32B2;} + .d2-3917036827 .fill-B2{fill:#0D32B2;} + .d2-3917036827 .fill-B3{fill:#E3E9FD;} + .d2-3917036827 .fill-B4{fill:#E3E9FD;} + .d2-3917036827 .fill-B5{fill:#EDF0FD;} + .d2-3917036827 .fill-B6{fill:#F7F8FE;} + .d2-3917036827 .fill-AA2{fill:#4A6FF3;} + .d2-3917036827 .fill-AA4{fill:#EDF0FD;} + .d2-3917036827 .fill-AA5{fill:#F7F8FE;} + .d2-3917036827 .fill-AB4{fill:#EDF0FD;} + .d2-3917036827 .fill-AB5{fill:#F7F8FE;} + .d2-3917036827 .stroke-N1{stroke:#0A0F25;} + .d2-3917036827 .stroke-N2{stroke:#676C7E;} + .d2-3917036827 .stroke-N3{stroke:#9499AB;} + .d2-3917036827 .stroke-N4{stroke:#CFD2DD;} + .d2-3917036827 .stroke-N5{stroke:#DEE1EB;} + .d2-3917036827 .stroke-N6{stroke:#EEF1F8;} + .d2-3917036827 .stroke-N7{stroke:#FFFFFF;} + .d2-3917036827 .stroke-B1{stroke:#0D32B2;} + .d2-3917036827 .stroke-B2{stroke:#0D32B2;} + .d2-3917036827 .stroke-B3{stroke:#E3E9FD;} + .d2-3917036827 .stroke-B4{stroke:#E3E9FD;} + .d2-3917036827 .stroke-B5{stroke:#EDF0FD;} + .d2-3917036827 .stroke-B6{stroke:#F7F8FE;} + .d2-3917036827 .stroke-AA2{stroke:#4A6FF3;} + .d2-3917036827 .stroke-AA4{stroke:#EDF0FD;} + .d2-3917036827 .stroke-AA5{stroke:#F7F8FE;} + .d2-3917036827 .stroke-AB4{stroke:#EDF0FD;} + .d2-3917036827 .stroke-AB5{stroke:#F7F8FE;} + .d2-3917036827 .background-color-N1{background-color:#0A0F25;} + .d2-3917036827 .background-color-N2{background-color:#676C7E;} + .d2-3917036827 .background-color-N3{background-color:#9499AB;} + .d2-3917036827 .background-color-N4{background-color:#CFD2DD;} + .d2-3917036827 .background-color-N5{background-color:#DEE1EB;} + .d2-3917036827 .background-color-N6{background-color:#EEF1F8;} + .d2-3917036827 .background-color-N7{background-color:#FFFFFF;} + .d2-3917036827 .background-color-B1{background-color:#0D32B2;} + .d2-3917036827 .background-color-B2{background-color:#0D32B2;} + .d2-3917036827 .background-color-B3{background-color:#E3E9FD;} + .d2-3917036827 .background-color-B4{background-color:#E3E9FD;} + .d2-3917036827 .background-color-B5{background-color:#EDF0FD;} + .d2-3917036827 .background-color-B6{background-color:#F7F8FE;} + .d2-3917036827 .background-color-AA2{background-color:#4A6FF3;} + .d2-3917036827 .background-color-AA4{background-color:#EDF0FD;} + .d2-3917036827 .background-color-AA5{background-color:#F7F8FE;} + .d2-3917036827 .background-color-AB4{background-color:#EDF0FD;} + .d2-3917036827 .background-color-AB5{background-color:#F7F8FE;} + .d2-3917036827 .color-N1{color:#0A0F25;} + .d2-3917036827 .color-N2{color:#676C7E;} + .d2-3917036827 .color-N3{color:#9499AB;} + .d2-3917036827 .color-N4{color:#CFD2DD;} + .d2-3917036827 .color-N5{color:#DEE1EB;} + .d2-3917036827 .color-N6{color:#EEF1F8;} + .d2-3917036827 .color-N7{color:#FFFFFF;} + .d2-3917036827 .color-B1{color:#0D32B2;} + .d2-3917036827 .color-B2{color:#0D32B2;} + .d2-3917036827 .color-B3{color:#E3E9FD;} + .d2-3917036827 .color-B4{color:#E3E9FD;} + .d2-3917036827 .color-B5{color:#EDF0FD;} + .d2-3917036827 .color-B6{color:#F7F8FE;} + .d2-3917036827 .color-AA2{color:#4A6FF3;} + .d2-3917036827 .color-AA4{color:#EDF0FD;} + .d2-3917036827 .color-AA5{color:#F7F8FE;} + .d2-3917036827 .color-AB4{color:#EDF0FD;} + .d2-3917036827 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>windowroofgarage \ No newline at end of file diff --git a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg index f3e7aafaff..0a58bd0b7d 100644 --- a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg +++ b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg @@ -1,9 +1,9 @@ -windowroofgarage + .d2-2455851432 .fill-N1{fill:#0A0F25;} + .d2-2455851432 .fill-N2{fill:#676C7E;} + .d2-2455851432 .fill-N3{fill:#9499AB;} + .d2-2455851432 .fill-N4{fill:#CFD2DD;} + .d2-2455851432 .fill-N5{fill:#DEE1EB;} + .d2-2455851432 .fill-N6{fill:#EEF1F8;} + .d2-2455851432 .fill-N7{fill:#FFFFFF;} + .d2-2455851432 .fill-B1{fill:#0D32B2;} + .d2-2455851432 .fill-B2{fill:#0D32B2;} + .d2-2455851432 .fill-B3{fill:#E3E9FD;} + .d2-2455851432 .fill-B4{fill:#E3E9FD;} + .d2-2455851432 .fill-B5{fill:#EDF0FD;} + .d2-2455851432 .fill-B6{fill:#F7F8FE;} + .d2-2455851432 .fill-AA2{fill:#4A6FF3;} + .d2-2455851432 .fill-AA4{fill:#EDF0FD;} + .d2-2455851432 .fill-AA5{fill:#F7F8FE;} + .d2-2455851432 .fill-AB4{fill:#EDF0FD;} + .d2-2455851432 .fill-AB5{fill:#F7F8FE;} + .d2-2455851432 .stroke-N1{stroke:#0A0F25;} + .d2-2455851432 .stroke-N2{stroke:#676C7E;} + .d2-2455851432 .stroke-N3{stroke:#9499AB;} + .d2-2455851432 .stroke-N4{stroke:#CFD2DD;} + .d2-2455851432 .stroke-N5{stroke:#DEE1EB;} + .d2-2455851432 .stroke-N6{stroke:#EEF1F8;} + .d2-2455851432 .stroke-N7{stroke:#FFFFFF;} + .d2-2455851432 .stroke-B1{stroke:#0D32B2;} + .d2-2455851432 .stroke-B2{stroke:#0D32B2;} + .d2-2455851432 .stroke-B3{stroke:#E3E9FD;} + .d2-2455851432 .stroke-B4{stroke:#E3E9FD;} + .d2-2455851432 .stroke-B5{stroke:#EDF0FD;} + .d2-2455851432 .stroke-B6{stroke:#F7F8FE;} + .d2-2455851432 .stroke-AA2{stroke:#4A6FF3;} + .d2-2455851432 .stroke-AA4{stroke:#EDF0FD;} + .d2-2455851432 .stroke-AA5{stroke:#F7F8FE;} + .d2-2455851432 .stroke-AB4{stroke:#EDF0FD;} + .d2-2455851432 .stroke-AB5{stroke:#F7F8FE;} + .d2-2455851432 .background-color-N1{background-color:#0A0F25;} + .d2-2455851432 .background-color-N2{background-color:#676C7E;} + .d2-2455851432 .background-color-N3{background-color:#9499AB;} + .d2-2455851432 .background-color-N4{background-color:#CFD2DD;} + .d2-2455851432 .background-color-N5{background-color:#DEE1EB;} + .d2-2455851432 .background-color-N6{background-color:#EEF1F8;} + .d2-2455851432 .background-color-N7{background-color:#FFFFFF;} + .d2-2455851432 .background-color-B1{background-color:#0D32B2;} + .d2-2455851432 .background-color-B2{background-color:#0D32B2;} + .d2-2455851432 .background-color-B3{background-color:#E3E9FD;} + .d2-2455851432 .background-color-B4{background-color:#E3E9FD;} + .d2-2455851432 .background-color-B5{background-color:#EDF0FD;} + .d2-2455851432 .background-color-B6{background-color:#F7F8FE;} + .d2-2455851432 .background-color-AA2{background-color:#4A6FF3;} + .d2-2455851432 .background-color-AA4{background-color:#EDF0FD;} + .d2-2455851432 .background-color-AA5{background-color:#F7F8FE;} + .d2-2455851432 .background-color-AB4{background-color:#EDF0FD;} + .d2-2455851432 .background-color-AB5{background-color:#F7F8FE;} + .d2-2455851432 .color-N1{color:#0A0F25;} + .d2-2455851432 .color-N2{color:#676C7E;} + .d2-2455851432 .color-N3{color:#9499AB;} + .d2-2455851432 .color-N4{color:#CFD2DD;} + .d2-2455851432 .color-N5{color:#DEE1EB;} + .d2-2455851432 .color-N6{color:#EEF1F8;} + .d2-2455851432 .color-N7{color:#FFFFFF;} + .d2-2455851432 .color-B1{color:#0D32B2;} + .d2-2455851432 .color-B2{color:#0D32B2;} + .d2-2455851432 .color-B3{color:#E3E9FD;} + .d2-2455851432 .color-B4{color:#E3E9FD;} + .d2-2455851432 .color-B5{color:#EDF0FD;} + .d2-2455851432 .color-B6{color:#F7F8FE;} + .d2-2455851432 .color-AA2{color:#4A6FF3;} + .d2-2455851432 .color-AA4{color:#EDF0FD;} + .d2-2455851432 .color-AA5{color:#F7F8FE;} + .d2-2455851432 .color-AB4{color:#EDF0FD;} + .d2-2455851432 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>windowroofgarage \ No newline at end of file