implement sketch

This commit is contained in:
Alexander Wang 2022-12-20 23:43:45 -08:00
parent 0e4a8d450d
commit 07fb1a3d86
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
215 changed files with 3762 additions and 64 deletions

View file

@ -1,5 +1,7 @@
#### Features 🚀
- `sketch` flag renders the diagram to look like it was sketched by hand. [#492](https://github.com/terrastruct/d2/pull/492)
#### Improvements 🧹
- Improved label placements for shapes with images to avoid overlapping container labels. [#474](https://github.com/terrastruct/d2/pull/474)

View file

@ -58,6 +58,9 @@ Port listening address when used with
Set the diagram theme to the passed integer. For a list of available options, see
.Lk https://oss.terrastruct.com/d2
.Ns .
.It Fl s , -sketch Ar false
Renders the diagram to look like it was sketched by hand
.Ns .
.It Fl -pad Ar 100
Pixels padded around the rendered diagram
.Ns .

View file

@ -120,7 +120,7 @@ func test(t *testing.T, textPath, text string) {
ruler, err := textmeasure.NewRuler()
assert.Nil(t, err)
err = g.SetDimensions(nil, ruler)
err = g.SetDimensions(nil, ruler, nil)
assert.Nil(t, err)
err = d2dagrelayout.Layout(ctx, g)
@ -128,7 +128,7 @@ func test(t *testing.T, textPath, text string) {
t.Fatal(err)
}
_, err = d2exporter.Export(ctx, g, 0)
_, err = d2exporter.Export(ctx, g, 0, nil)
if err != nil {
t.Fatal(err)
}

View file

@ -5,15 +5,21 @@ import (
"strconv"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
)
func Export(ctx context.Context, g *d2graph.Graph, themeID int64) (*d2target.Diagram, error) {
func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
theme := d2themescatalog.Find(themeID)
diagram := d2target.NewDiagram()
if fontFamily == nil {
defaultFont := d2fonts.SourceSansPro
fontFamily = &defaultFont
}
diagram.FontFamily = fontFamily
diagram.Shapes = make([]d2target.Shape, len(g.Objects))
for i := range g.Objects {

View file

@ -216,7 +216,7 @@ func run(t *testing.T, tc testCase) {
ruler, err := textmeasure.NewRuler()
assert.JSON(t, nil, err)
err = g.SetDimensions(nil, ruler)
err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err)
err = d2dagrelayout.Layout(ctx, g)
@ -224,7 +224,7 @@ func run(t *testing.T, tc testCase) {
t.Fatal(err)
}
got, err := d2exporter.Export(ctx, g, tc.themeID)
got, err := d2exporter.Export(ctx, g, tc.themeID, nil)
if err != nil {
t.Fatal(err)
}

View file

@ -841,7 +841,7 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
return nil, fmt.Errorf("text not pre-measured and no ruler provided")
}
func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText) *d2target.TextDimensions {
func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions {
if dims := findMeasured(mtexts, t); dims != nil {
return dims
}
@ -861,7 +861,11 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
} else if t.IsItalic {
style = d2fonts.FONT_STYLE_ITALIC
}
w, h = ruler.Measure(d2fonts.SourceSansPro.Font(t.FontSize, style), t.Text)
if fontFamily == nil {
defaultFont := d2fonts.SourceSansPro
fontFamily = &defaultFont
}
w, h = ruler.Measure(fontFamily.Font(t.FontSize, style), t.Text)
}
return d2target.NewTextDimensions(w, h)
}
@ -870,13 +874,13 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2
}
func appendTextDedup(texts []*d2target.MText, t *d2target.MText) []*d2target.MText {
if getTextDimensions(texts, nil, t) == nil {
if getTextDimensions(texts, nil, t, nil) == nil {
return append(texts, t)
}
return texts
}
func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler) error {
func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) error {
for _, obj := range g.Objects {
obj.Box = &geo.Box{}
// TODO fix edge cases for unnamed class etc
@ -905,7 +909,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
innerLabelPadding = 0
} else {
dims = getTextDimensions(mtexts, ruler, obj.Text())
dims = getTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
}
if dims == nil {
if obj.Attributes.Shape.Value == d2target.ShapeImage {
@ -959,7 +963,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
maxWidth := dims.Width
for _, f := range obj.Class.Fields {
fdims := getTextDimensions(mtexts, ruler, f.Text())
fdims := getTextDimensions(mtexts, ruler, f.Text(), fontFamily)
if fdims == nil {
return fmt.Errorf("dimensions for class field %#v not found", f.Text())
}
@ -969,7 +973,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
for _, m := range obj.Class.Methods {
mdims := getTextDimensions(mtexts, ruler, m.Text())
mdims := getTextDimensions(mtexts, ruler, m.Text(), fontFamily)
if mdims == nil {
return fmt.Errorf("dimensions for class method %#v not found", m.Text())
}
@ -988,7 +992,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
if anyRowText != nil {
// 10px of padding top and bottom so text doesn't look squished
rowHeight := getTextDimensions(mtexts, ruler, anyRowText).Height + 20
rowHeight := getTextDimensions(mtexts, ruler, anyRowText, fontFamily).Height + 20
obj.Height = float64(rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2))
}
// Leave room for padding
@ -1043,7 +1047,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
for _, label := range endpointLabels {
t := edge.Text()
t.Text = label
dims := getTextDimensions(mtexts, ruler, t)
dims := getTextDimensions(mtexts, ruler, t, fontFamily)
edge.MinWidth += dims.Width
// Some padding as it's not totally near the end
edge.MinHeight += dims.Height + 5
@ -1053,7 +1057,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
dims := getTextDimensions(mtexts, ruler, edge.Text())
dims := getTextDimensions(mtexts, ruler, edge.Text(), fontFamily)
if dims == nil {
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
}

View file

@ -10,6 +10,7 @@ import (
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/textmeasure"
)
@ -20,7 +21,13 @@ type CompileOptions struct {
Ruler *textmeasure.Ruler
Layout func(context.Context, *d2graph.Graph) error
ThemeID int64
// FontFamily controls the font family used for all texts that are not the following:
// - code
// - latex
// - pre-measured (web setting)
// TODO maybe some will want to configure code font too, but that's much lower priority
FontFamily *d2fonts.FontFamily
ThemeID int64
}
func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) {
@ -36,7 +43,7 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}
if len(g.Objects) > 0 {
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler)
err = g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
if err != nil {
return nil, nil, err
}
@ -48,7 +55,7 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}
}
diagram, err := d2exporter.Export(ctx, g, opts.ThemeID)
diagram, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily)
return diagram, g, err
}

View file

@ -1,6 +1,7 @@
// d2fonts holds fonts for renderings
// TODO write a script to do this as part of CI
// Currently using an online converter: https://dopiaza.org/tools/datauri/index.php
package d2fonts
import (
@ -8,7 +9,7 @@ import (
"strings"
)
type FontFamily int
type FontFamily string
type FontStyle string
type Font struct {
@ -38,8 +39,9 @@ const (
FONT_STYLE_BOLD FontStyle = "bold"
FONT_STYLE_ITALIC FontStyle = "italic"
SourceSansPro FontFamily = iota
SourceCodePro FontFamily = iota
SourceSansPro FontFamily = "SourceSansPro"
SourceCodePro FontFamily = "SourceCodePro"
HandDrawn FontFamily = "HandDrawn"
)
var FontSizes = []int{
@ -61,6 +63,7 @@ var FontStyles = []FontStyle{
var FontFamilies = []FontFamily{
SourceSansPro,
SourceCodePro,
HandDrawn,
}
//go:embed encoded/SourceSansPro-Regular.txt
@ -75,6 +78,12 @@ var sourceSansProItalicBase64 string
//go:embed encoded/SourceCodePro-Regular.txt
var sourceCodeProRegularBase64 string
//go:embed encoded/ArchitectsDaughter-Regular.txt
var architectsDaughterRegularBase64 string
//go:embed encoded/FuzzyBubbles-Bold.txt
var fuzzyBubblesBoldBase64 string
//go:embed ttf/*
var fontFacesFS embed.FS
@ -99,6 +108,19 @@ func init() {
Family: SourceCodePro,
Style: FONT_STYLE_REGULAR,
}: sourceCodeProRegularBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_REGULAR,
}: architectsDaughterRegularBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_ITALIC,
// This font has no italic, so just reuse regular
}: architectsDaughterRegularBase64,
{
Family: HandDrawn,
Style: FONT_STYLE_BOLD,
}: fuzzyBubblesBoldBase64,
}
for k, v := range FontEncodings {
@ -138,4 +160,24 @@ func init() {
Family: SourceSansPro,
Style: FONT_STYLE_ITALIC,
}] = b
b, err = fontFacesFS.ReadFile("ttf/ArchitectsDaughter-Regular.ttf")
if err != nil {
panic(err)
}
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_REGULAR,
}] = b
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_ITALIC,
}] = b
b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
if err != nil {
panic(err)
}
FontFaces[Font{
Family: HandDrawn,
Style: FONT_STYLE_BOLD,
}] = b
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
const root = {
ownerDocument: {
createElementNS: (ns, tagName) => {
const children = [];
const attrs = {};
const style = {};
return {
style,
tagName,
attrs,
setAttribute: (key, value) => (attrs[key] = value),
appendChild: (node) => children.push(node),
children,
};
},
},
};
const rc = rough.svg(root, { seed: 1 });
let node;

View file

@ -0,0 +1,227 @@
package d2sketch
import (
"encoding/json"
"fmt"
_ "embed"
"github.com/dop251/goja"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/svg"
)
//go:embed fillpattern.svg
var fillPattern string
//go:embed rough.js
var roughJS string
//go:embed setup.js
var setupJS string
type Runner goja.Runtime
var baseRoughProps = `fillWeight: 2.0,
hachureGap: 16,
fillStyle: "solid",
bowing: 2,
seed: 1,`
func (r *Runner) run(js string) (goja.Value, error) {
vm := (*goja.Runtime)(r)
return vm.RunString(js)
}
func InitSketchVM() (*Runner, error) {
vm := goja.New()
if _, err := vm.RunString(roughJS); err != nil {
return nil, err
}
if _, err := vm.RunString(setupJS); err != nil {
return nil, err
}
r := Runner(*vm)
return &r, nil
}
// DefineFillPattern adds a reusable pattern that is overlayed on shapes with
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
// not distractingly so.
func DefineFillPattern() string {
return fmt.Sprintf(`<defs>
<pattern id="streaks"
x="0" y="0" width="100" height="100"
patternUnits="userSpaceOnUse" >
%s
</pattern>
</defs>`, fillPattern)
}
func shapeStyle(shape d2target.Shape) string {
out := ""
out += fmt.Sprintf(`fill:%s;`, shape.Fill)
out += fmt.Sprintf(`stroke:%s;`, shape.Stroke)
out += fmt.Sprintf(`opacity:%f;`, shape.Opacity)
out += fmt.Sprintf(`stroke-width:%d;`, shape.StrokeWidth)
if shape.StrokeDash != 0 {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
}
return out
}
func Rect(r *Runner, shape d2target.Shape) (string, error) {
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
fill: "%s",
stroke: "%s",
strokeWidth: %d,
%s
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
if _, err := r.run(js); err != nil {
return "", err
}
paths, err := extractPaths(r)
if err != nil {
return "", err
}
output := ""
for _, p := range paths {
output += fmt.Sprintf(
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
}
output += fmt.Sprintf(
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`,
shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
)
return output, nil
}
func Oval(r *Runner, shape d2target.Shape) (string, error) {
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
fill: "%s",
stroke: "%s",
strokeWidth: %d,
%s
});`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
if _, err := r.run(js); err != nil {
return "", err
}
paths, err := extractPaths(r)
if err != nil {
return "", err
}
output := ""
for _, p := range paths {
output += fmt.Sprintf(
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
}
output += fmt.Sprintf(
`<ellipse class="sketch-overlay" transform="translate(%d %d)" rx="%d" ry="%d" />`,
shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2, shape.Width/2, shape.Height/2,
)
return output, nil
}
// TODO need to personalize this per shape like we do in Terrastruct app
func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
output := ""
for _, path := range paths {
js := fmt.Sprintf(`node = rc.path("%s", {
fill: "%s",
stroke: "%s",
strokeWidth: %d,
%s
});`, path, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps)
if _, err := r.run(js); err != nil {
return "", err
}
sketchPaths, err := extractPaths(r)
if err != nil {
return "", err
}
for _, p := range sketchPaths {
output += fmt.Sprintf(
`<path class="shape" d="%s" style="%s" />`,
p, shapeStyle(shape),
)
}
for _, p := range sketchPaths {
output += fmt.Sprintf(
`<path class="sketch-overlay" d="%s" />`,
p,
)
}
}
return output, nil
}
func connectionStyle(connection d2target.Connection) string {
out := ""
out += fmt.Sprintf(`stroke:%s;`, connection.Stroke)
out += fmt.Sprintf(`opacity:%f;`, connection.Opacity)
out += fmt.Sprintf(`stroke-width:%d;`, connection.StrokeWidth)
if connection.StrokeDash != 0 {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
}
return out
}
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
roughness := 1.0
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
if _, err := r.run(js); err != nil {
return "", err
}
paths, err := extractPaths(r)
if err != nil {
return "", err
}
output := ""
for _, p := range paths {
output += fmt.Sprintf(
`<path class="connection" fill="none" d="%s" style="%s" %s/>`,
p, connectionStyle(connection), attrs,
)
}
return output, nil
}
type attrs struct {
D string `json:"d"`
}
type node struct {
Attrs attrs `json:"attrs"`
}
func extractPaths(r *Runner) ([]string, error) {
val, err := r.run("JSON.stringify(node.children)")
if err != nil {
return nil, err
}
var nodes []node
err = json.Unmarshal([]byte(val.String()), &nodes)
if err != nil {
return nil, err
}
var paths []string
for _, n := range nodes {
paths = append(paths, n.Attrs.D)
}
return paths, nil
}

View file

@ -0,0 +1,292 @@
package d2sketch_test
import (
"context"
"encoding/xml"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"cdr.dev/slog"
tassert "github.com/stretchr/testify/assert"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
func TestSketch(t *testing.T) {
t.Parallel()
tcs := []testCase{
{
name: "basic",
script: `a -> b
`,
},
{
name: "child to child",
script: `winter.snow -> summer.sun
`,
},
{
name: "connection label",
script: `a -> b: hello
`,
},
{
name: "chess",
script: `timeline mixer: "" {
explanation: |md
## **Timeline mixer**
- Inject ads, who-to-follow, onboarding
- Conversation module
- Cursoring,pagination
- Tweat deduplication
- Served data logging
|
}
People discovery: "People discovery \nservice"
admixer: Ad mixer {
fill: "#c1a2f3"
}
onboarding service: "Onboarding \nservice"
timeline mixer -> People discovery
timeline mixer -> onboarding service
timeline mixer -> admixer
container0: "" {
graphql
comment
tlsapi
}
container0.graphql: GraphQL\nFederated Strato Column {
shape: image
icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
}
container0.comment: |md
## Tweet/user content hydration, visibility filtering
|
container0.tlsapi: TLS-API (being deprecated)
container0.graphql -> timeline mixer
timeline mixer <- container0.tlsapi
twitter fe: "Twitter Frontend " {
icon: https://icons.terrastruct.com/social/013-twitter-1.svg
shape: image
}
twitter fe -> container0.graphql: iPhone web
twitter fe -> container0.tlsapi: HTTP Android
web: Web {
icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
shape: image
}
Iphone: {
icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
shape: image
}
Android: {
icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
shape: image
}
web -> twitter fe
timeline scorer: "Timeline\nScorer" {
fill: "#ffdef1"
}
home ranker: Home Ranker
timeline service: Timeline Service
timeline mixer -> timeline scorer: Thrift RPC
timeline mixer -> home ranker: {
style.stroke-dash: 4
style.stroke: "#000E3D"
}
timeline mixer -> timeline service
home mixer: Home mixer {
# fill: "#c1a2f3"
}
container0.graphql -> home mixer: {
style.stroke-dash: 4
style.stroke: "#000E3D"
}
home mixer -> timeline scorer
home mixer -> home ranker: {
style.stroke-dash: 4
style.stroke: "#000E3D"
}
home mixer -> timeline service
manhattan 2: Manhattan
gizmoduck: Gizmoduck
socialgraph: Social graph
tweetypie: Tweety Pie
home mixer -> manhattan 2
home mixer -> gizmoduck
home mixer -> socialgraph
home mixer -> tweetypie
Iphone -> twitter fe
Android -> twitter fe
prediction service2: Prediction Service {
shape: image
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
}
home scorer: Home Scorer {
fill: "#ffdef1"
}
manhattan: Manhattan
memcache: Memcache {
icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
}
fetch: Fetch {
multiple: true
shape: step
}
feature: Feature {
multiple: true
shape: step
}
scoring: Scoring {
multiple: true
shape: step
}
fetch -> feature
feature -> scoring
prediction service: Prediction Service {
shape: image
icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
}
scoring -> prediction service
fetch -> container2.crmixer
home scorer -> manhattan: ""
home scorer -> memcache: ""
home scorer -> prediction service2
home ranker -> home scorer
home ranker -> container2.crmixer: Candidate Fetch
container2: "" {
style.stroke: "#000E3D"
style.fill: "#ffffff"
crmixer: CrMixer {
style.fill: "#F7F8FE"
}
earlybird: EarlyBird
utag: Utag
space: Space
communities: Communities
}
etc: ...etc
home scorer -> etc: Feature Hydration
feature -> manhattan
feature -> memcache
feature -> etc: Candidate sources
`,
},
{
name: "all_shapes",
script: `
rectangle: {shape: "rectangle"}
square: {shape: "square"}
page: {shape: "page"}
parallelogram: {shape: "parallelogram"}
document: {shape: "document"}
cylinder: {shape: "cylinder"}
queue: {shape: "queue"}
package: {shape: "package"}
step: {shape: "step"}
callout: {shape: "callout"}
stored_data: {shape: "stored_data"}
person: {shape: "person"}
diamond: {shape: "diamond"}
oval: {shape: "oval"}
circle: {shape: "circle"}
hexagon: {shape: "hexagon"}
cloud: {shape: "cloud"}
rectangle -> square -> page
parallelogram -> document -> cylinder
queue -> package -> step
callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
`,
},
}
runa(t, tcs)
}
type testCase struct {
name string
script string
skip bool
}
func runa(t *testing.T, tcs []testCase) {
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
t.Parallel()
run(t, tc)
})
}
}
func run(t *testing.T, tc testCase) {
ctx := context.Background()
ctx = log.WithTB(ctx, t, nil)
ctx = log.Leveled(ctx, slog.LevelDebug)
ruler, err := textmeasure.NewRuler()
if !tassert.Nil(t, err) {
return
}
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler,
ThemeID: 0,
Layout: d2dagrelayout.Layout,
FontFamily: go2.Pointer(d2fonts.HandDrawn),
})
if !tassert.Nil(t, err) {
return
}
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/"))
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING,
Sketch: true,
})
assert.Success(t, err)
err = os.MkdirAll(dataPath, 0755)
assert.Success(t, err)
err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
assert.Success(t, err)
defer os.Remove(pathGotSVG)
var xmlParsed interface{}
err = xml.Unmarshal(svgBytes, &xmlParsed)
assert.Success(t, err)
err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
assert.Success(t, err)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 298 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 196 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 649 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 382 KiB

View file

@ -0,0 +1,136 @@
{
"name": "",
"fontFamily": "HandDrawn",
"shapes": [
{
"id": "a",
"type": "",
"pos": {
"x": 1,
"y": 0
},
"width": 114,
"height": 126,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F7F8FE",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "a",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 14,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "",
"pos": {
"x": 0,
"y": 226
},
"width": 115,
"height": 126,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F7F8FE",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "b",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 15,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(a -> b)[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "b",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "hello",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 31,
"labelHeight": 23,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 57.5,
"y": 126
},
{
"x": 57.5,
"y": 166
},
{
"x": 57.5,
"y": 186
},
{
"x": 57.5,
"y": 226
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 378 KiB

View file

@ -25,11 +25,13 @@ import (
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/d2sketch"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/svg"
"oss.terrastruct.com/d2/lib/textmeasure"
)
@ -44,9 +46,17 @@ var multipleOffset = geo.NewVector(10, -10)
//go:embed style.css
var styleCSS string
//go:embed sketchstyle.css
var sketchStyleCSS string
//go:embed github-markdown.css
var mdCSS string
type RenderOpts struct {
Pad int
Sketch bool
}
func setViewbox(writer io.Writer, diagram *d2target.Diagram, pad int) (width int, height int) {
tl, br := diagram.BoundingBox()
w := br.X - tl.X + pad*2
@ -346,7 +356,7 @@ func makeLabelMask(labelTL *geo.Point, width, height int) string {
)
}
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape) (labelMask string) {
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) {
fmt.Fprintf(writer, `<g id="%s">`, escapeText(connection.ID))
var markerStart string
if connection.SrcArrow != d2target.NoArrowhead {
@ -413,13 +423,22 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
}
}
fmt.Fprintf(writer, `<path d="%s" class="connection" style="fill:none;%s" %s%smask="url(#%s)"/>`,
pathData(connection, idToShape),
connectionStyle(connection),
path := pathData(connection, idToShape)
attrs := fmt.Sprintf(`%s%smask="url(#%s)"`,
markerStart,
markerEnd,
labelMaskID,
)
if sketchRunner != nil {
out, err := d2sketch.Connection(sketchRunner, connection, path, attrs)
if err != nil {
return "", err
}
fmt.Fprintf(writer, out)
} else {
fmt.Fprintf(writer, `<path d="%s" class="connection" style="fill:none;%s" %s/>`,
path, connectionStyle(connection), attrs)
}
if connection.Label != "" {
fontClass := "text"
@ -589,7 +608,7 @@ func render3dRect(targetShape d2target.Shape) string {
return borderMask + mainRect + renderedSides + renderedBorder
}
func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string, err error) {
func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) {
fmt.Fprintf(writer, `<g id="%s">`, escapeText(targetShape.ID))
tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
width := float64(targetShape.Width)
@ -636,7 +655,15 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
if targetShape.Multiple {
fmt.Fprint(writer, renderOval(multipleTL, width, height, style))
}
fmt.Fprint(writer, renderOval(tl, width, height, style))
if sketchRunner != nil {
out, err := d2sketch.Oval(sketchRunner, targetShape)
if err != nil {
return "", err
}
fmt.Fprintf(writer, out)
} else {
fmt.Fprint(writer, renderOval(tl, width, height, style))
}
case d2target.ShapeImage:
fmt.Fprintf(writer, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`,
@ -652,8 +679,16 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
targetShape.Pos.X+10, targetShape.Pos.Y-10, targetShape.Width, targetShape.Height, style)
}
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
if sketchRunner != nil {
out, err := d2sketch.Rect(sketchRunner, targetShape)
if err != nil {
return "", err
}
fmt.Fprintf(writer, out)
} else {
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style)
}
}
case d2target.ShapeText, d2target.ShapeCode:
default:
@ -664,8 +699,16 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) (labelMask string,
}
}
for _, pathData := range s.GetSVGPathData() {
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, pathData, style)
if sketchRunner != nil {
out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
if err != nil {
return "", err
}
fmt.Fprintf(writer, out)
} else {
for _, pathData := range s.GetSVGPathData() {
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, pathData, style)
}
}
}
@ -841,7 +884,7 @@ func shapeStyle(shape d2target.Shape) string {
out += fmt.Sprintf(`opacity:%f;`, shape.Opacity)
out += fmt.Sprintf(`stroke-width:%d;`, shape.StrokeWidth)
if shape.StrokeDash != 0 {
dashSize, gapSize := getStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
}
@ -855,22 +898,14 @@ func connectionStyle(connection d2target.Connection) string {
out += fmt.Sprintf(`opacity:%f;`, connection.Opacity)
out += fmt.Sprintf(`stroke-width:%d;`, connection.StrokeWidth)
if connection.StrokeDash != 0 {
dashSize, gapSize := getStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
}
return out
}
func getStrokeDashAttributes(strokeWidth, dashGapSize float64) (float64, float64) {
// as the stroke width gets thicker, the dash gap gets smaller
scale := math.Log10(-0.6*strokeWidth+10.6)*0.5 + 0.5
scaledDashSize := strokeWidth * dashGapSize
scaledGapSize := scale * scaledDashSize
return scaledDashSize, scaledGapSize
}
func embedFonts(buf *bytes.Buffer) {
func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
content := buf.String()
buf.WriteString(`<style type="text/css"><![CDATA[`)
@ -889,7 +924,7 @@ func embedFonts(buf *bytes.Buffer) {
font-family: font-regular;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)])
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR)])
break
}
}
@ -910,7 +945,7 @@ func embedFonts(buf *bytes.Buffer) {
font-family: font-bold;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)])
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD)])
break
}
}
@ -931,7 +966,7 @@ func embedFonts(buf *bytes.Buffer) {
font-family: font-italic;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_ITALIC)])
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC)])
break
}
}
@ -963,15 +998,32 @@ func embedFonts(buf *bytes.Buffer) {
}
// TODO minify output at end
func Render(diagram *d2target.Diagram, pad int) ([]byte, error) {
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
var sketchRunner *d2sketch.Runner
pad := DEFAULT_PADDING
if opts != nil {
pad = opts.Pad
if opts.Sketch {
var err error
sketchRunner, err = d2sketch.InitSketchVM()
if err != nil {
return nil, err
}
}
}
buf := &bytes.Buffer{}
w, h := setViewbox(buf, diagram, pad)
styleCSS2 := ""
if sketchRunner != nil {
styleCSS2 = "\n" + sketchStyleCSS
}
buf.WriteString(fmt.Sprintf(`<style type="text/css">
<![CDATA[
%s
%s%s
]]>
</style>`, styleCSS))
</style>`, styleCSS, styleCSS2))
hasMarkdown := false
for _, s := range diagram.Shapes {
@ -983,6 +1035,9 @@ func Render(diagram *d2target.Diagram, pad int) ([]byte, error) {
if hasMarkdown {
fmt.Fprintf(buf, `<style type="text/css">%s</style>`, mdCSS)
}
if sketchRunner != nil {
fmt.Fprintf(buf, d2sketch.DefineFillPattern())
}
// only define shadow filter if a shape uses it
for _, s := range diagram.Shapes {
@ -1017,12 +1072,15 @@ func Render(diagram *d2target.Diagram, pad int) ([]byte, error) {
markers := map[string]struct{}{}
for _, obj := range allObjects {
if c, is := obj.(d2target.Connection); is {
labelMask := drawConnection(buf, labelMaskID, c, markers, idToShape)
labelMask, err := drawConnection(buf, labelMaskID, c, markers, idToShape, sketchRunner)
if err != nil {
return nil, err
}
if labelMask != "" {
labelMasks = append(labelMasks, labelMask)
}
} else if s, is := obj.(d2target.Shape); is {
labelMask, err := drawShape(buf, s)
labelMask, err := drawShape(buf, s, sketchRunner)
if err != nil {
return nil, err
} else if labelMask != "" {
@ -1046,7 +1104,7 @@ func Render(diagram *d2target.Diagram, pad int) ([]byte, error) {
`</mask>`,
}, "\n"))
embedFonts(buf)
embedFonts(buf, diagram.FontFamily)
buf.WriteString(`</svg>`)
return buf.Bytes(), nil

View file

@ -0,0 +1,4 @@
.sketch-overlay {
fill: url(#streaks);
mix-blend-mode: overlay;
}

View file

@ -10,6 +10,7 @@ import (
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
@ -22,8 +23,9 @@ const (
)
type Diagram struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
FontFamily *d2fonts.FontFamily `json:"fontFamily,omitempty"`
Shapes []Shape `json:"shapes"`
Connections []Connection `json:"connections"`

View file

@ -20,6 +20,8 @@ func main() {
Ruler: ruler,
ThemeID: d2themescatalog.GrapeSoda.ID,
})
out, _ := d2svg.Render(diagram, d2svg.DEFAULT_PADDING)
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING,
})
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
}

View file

@ -18,9 +18,11 @@ import (
func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler)
_ = graph.SetDimensions(nil, ruler, nil)
_ = d2dagrelayout.Layout(context.Background(), graph)
diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID)
out, _ := d2svg.Render(diagram, d2svg.DEFAULT_PADDING)
diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID, nil)
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING,
})
_ = ioutil.WriteFile(filepath.Join("out.svg"), out, 0600)
}

View file

@ -125,7 +125,9 @@ func run(t *testing.T, tc testCase) {
dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestE2E/"), layoutName)
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
svgBytes, err := d2svg.Render(diagram, d2svg.DEFAULT_PADDING)
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING,
})
assert.Success(t, err)
err = os.MkdirAll(dataPath, 0755)
assert.Success(t, err)

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "\"ninety\\nnine\"",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "\"ninety\\nnine\"",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "A",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "A",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "foo",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "foo",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "b",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "b",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "table",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "table",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [],
"connections": []
}

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [],
"connections": []
}

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "rectangle",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "rectangle",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "rectangle",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "rectangle",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "rectangle",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "rectangle",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "c",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "c",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "aaa",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "aaa",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "aa",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "aa",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "manager",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "manager",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "hey",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "hey",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "finally",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "finally",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "b",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "b",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "alpha",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "alpha",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "size XS",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "size XS",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "md",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "md",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "md",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "md",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "ww",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "ww",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "aa",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "aa",

View file

@ -1,5 +1,6 @@
{
"name": "",
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",

Some files were not shown because too many files have changed in this diff Show more