Cleaned up the media folder and added a banner generator (#1027)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-07 19:34:54 +02:00
1 parent 8e0f04301a
commit f7451d0e38
16 files changed
+958 -24

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Downloaded during the setup, see README.md:
Inter.ttf
.venv/
__pycache__/
+46
View File
@@ -0,0 +1,46 @@
# Banner
`generate_banner.py` creates `app/MindWork AI Studio/wwwroot/svg/banner.svg`, the banner on the start page of the app and at the top of the repository's README.
The app shows the banner through an `<img>` element, where neither the app's CSS nor its web fonts apply. Therefore, the script converts every text into outlines, so the banner looks the same on Windows, macOS, Linux, and GitHub. The text uses [Inter](https://github.com/rsms/inter) (SIL Open Font License 1.1). The icons are the Material Icons the app itself uses, read from the MudBlazor package of the app (Apache License 2.0).
## Setup
You need Python 3 and a build of the app, so NuGet has restored MudBlazor into its package cache. The script reads the MudBlazor version and the target framework from the app's project file.
In this folder, create a virtual environment and install the packages:
```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```
Then download the font. The script expects this exact version of Inter and warns about any other one, because a different version changes the outlines:
```bash
curl -L -o Inter.ttf "https://raw.githubusercontent.com/google/fonts/e1d6480102fed30739fead0faee463101f892c8f/ofl/inter/Inter%5Bopsz,wght%5D.ttf"
```
Neither the font nor the virtual environment belongs in the repository; `.gitignore` keeps both out.
On Windows, use `.venv\Scripts\pip` and `.venv\Scripts\python` instead.
## Usage
```bash
.venv/bin/python generate_banner.py
```
The script overwrites the banner in the app. To write it somewhere else, for example to compare two versions, pass `--output <file>`. Without any change to the script, the output is identical to the committed banner.
On macOS, `qlmanage -t -s 1920 -o <folder> <file>` renders a preview with WebKit, the engine of the app on macOS. Any browser shows the file as well.
## Changing the banner
The content sits at the top of the script:
- `EYEBROW`, `NAME`, and `TAGLINE` form the wordmark on the left.
- `CHIPS` lists the eight capabilities on the right: two rows of four, where the chips of one column share their width. Pair labels of similar length in a column. The icon is the name of an icon in `Icons.Material.Filled`, ideally the one the app uses for that feature.
- `LAYOUT` holds positions and sizes, `CREAM` the color of text and icons.
Keep the labels in US English and short. Check the result in a narrow window as well: at 700 pixels, the labels are only about 10 pixels high. On wide windows, the start page crops up to 32 units at the top and the bottom, so text and chips must stay between y = 48 and y = 272.
+403
View File
@@ -0,0 +1,403 @@
#!/usr/bin/env python3
"""
Generates the banner of the start page and of the README:
app/MindWork AI Studio/wwwroot/svg/banner.svg
The app shows the banner through an <img> element, where neither the app's CSS nor its web fonts
apply. Therefore, every text is converted into outlines, so the banner looks the same on Windows,
macOS, Linux, and GitHub. The text uses Inter (SIL Open Font License 1.1); the icons are the
Material Icons the app itself uses, read from the MudBlazor package (Apache License 2.0).
See README.md in this folder for the setup.
"""
import argparse
import hashlib
import os
import re
import sys
from pathlib import Path
import dnfile
import uharfbuzz as hb
from fontTools.pens.basePen import BasePen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib import TTFont
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parents[1]
APP_PROJECT = REPO_ROOT / "app" / "MindWork AI Studio" / "MindWork AI Studio.csproj"
DEFAULT_OUTPUT = REPO_ROOT / "app" / "MindWork AI Studio" / "wwwroot" / "svg" / "banner.svg"
# The variable font Inter[opsz,wght].ttf from google/fonts, pinned to one commit. Another version
# of the font changes the outlines and therefore the generated file.
FONT_PATH = HERE / "Inter.ttf"
FONT_URL = "https://raw.githubusercontent.com/google/fonts/e1d6480102fed30739fead0faee463101f892c8f/ofl/inter/Inter%5Bopsz,wght%5D.ttf"
FONT_SHA256 = "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031"
#
# Content
#
EYEBROW = "MINDWORK"
NAME = "AI Studio"
TAGLINE = "Any model. Your data. Your rules."
# Two rows of four chips; the chips of one column share their width, so pair labels of similar
# length in a column. Each entry: (id, label, name of the icon in Icons.Material.Filled).
CHIPS = [
("chat", "Chat", "Chat"),
("assistants", "Assistants", "Apps"),
("builder", "No-code builder", "AutoMode"),
("tools", "Agentic tools", "Build"),
("research", "Research", "Language"),
("transcribe", "Transcribe", "Mic"),
("documents", "Your documents", "Description"),
("emails", "Your e-mails", "Email"),
]
#
# Look
#
WIDTH, HEIGHT, CORNER_RADIUS = 1920, 320, 24
CREAM = "#f6f0d8"
# When the window is wider than 1536 px, the start page crops up to 32 units at the top and at the
# bottom. Keep text and chips between y = 48 and y = 272.
LAYOUT = dict(
sun=(1860, -50, 660), # centre x, centre y, radius
left=104, eyebrow_y=98, eyebrow_size=30, name_y=208, name_size=120, tag_y=258, tag_size=34,
chip_font=29, chip_icon=30, chip_pad=22, chip_icon_gap=11, chip_h=62, chip_gap=14, chip_row_gap=14, chip_cy=164,
right=104, chip_fill="#0b2e22", chip_fill_op=.35, chip_stroke_op=.3,
)
# Weights and optical sizes of the variable font:
EYEBROW_STYLE = dict(wght=600, opsz=32)
NAME_STYLE = dict(wght=800, opsz=32)
TEXT_STYLE = dict(wght=500, opsz=22)
def num(v):
"""Formats a coordinate with one decimal and without needless zeros, e.g. 0.5 -> .5, 2.0 -> 2."""
s = f"{v:.1f}"
if s.endswith(".0"):
s = s[:-2]
if s.startswith("0."):
s = s[1:]
elif s.startswith("-0."):
s = "-" + s[2:]
return "0" if s in ("-0", "") else s
class RecordingPen(BasePen):
"""Records the outline of glyphs as absolute drawing operations."""
def __init__(self, glyph_set):
super().__init__(glyph_set)
self.ops = []
def _moveTo(self, p):
self.ops.append(("M", p))
def _lineTo(self, p):
self.ops.append(("L", p))
def _qCurveToOne(self, c, p):
self.ops.append(("Q", c, p))
def _curveToOne(self, c1, c2, p):
self.ops.append(("C", c1, c2, p))
def _closePath(self):
self.ops.append(("Z",))
def _endPath(self):
pass
def compact_path(ops):
"""
Turns recorded operations into short path data: relative commands, coordinates rounded to
tenths, and T wherever a quadratic control point mirrors the previous one, which is the case
for every implied on-curve point of TrueType outlines.
"""
# Rounding the absolute points first keeps the relative steps from drifting:
to_tenths = lambda p: (round(p[0] * 10), round(p[1] * 10))
out, last_cmd = [], None
cur = start = (0, 0)
prev_ctrl = None
def emit(cmd, values):
nonlocal last_cmd
body = ""
for value in values:
text = num(value / 10)
if body and not text.startswith("-"):
body += " "
body += text
# A repeated command may omit its letter:
if cmd == last_cmd and cmd not in ("m", "z"):
out.append((" " if not body.startswith("-") else "") + body)
else:
out.append(cmd + body)
# Further pairs after a moveto are linetos:
last_cmd = "l" if cmd == "m" else cmd
for op in ops:
kind = op[0]
if kind == "M":
p = to_tenths(op[1])
emit("m", (p[0] - cur[0], p[1] - cur[1]))
cur = start = p
prev_ctrl = None
elif kind == "L":
p = to_tenths(op[1])
dx, dy = p[0] - cur[0], p[1] - cur[1]
if dx == 0 and dy == 0:
pass
elif dy == 0:
emit("h", (dx,))
elif dx == 0:
emit("v", (dy,))
else:
emit("l", (dx, dy))
cur = p
prev_ctrl = None
elif kind == "Q":
c, p = to_tenths(op[1]), to_tenths(op[2])
mirrored = prev_ctrl is not None and abs(2 * cur[0] - prev_ctrl[0] - c[0]) <= 1 and abs(2 * cur[1] - prev_ctrl[1] - c[1]) <= 1
if mirrored:
emit("t", (p[0] - cur[0], p[1] - cur[1]))
c = (2 * cur[0] - prev_ctrl[0], 2 * cur[1] - prev_ctrl[1])
else:
emit("q", (c[0] - cur[0], c[1] - cur[1], p[0] - cur[0], p[1] - cur[1]))
prev_ctrl = c
cur = p
elif kind == "C":
c1, c2, p = to_tenths(op[1]), to_tenths(op[2]), to_tenths(op[3])
emit("c", (c1[0] - cur[0], c1[1] - cur[1], c2[0] - cur[0], c2[1] - cur[1], p[0] - cur[0], p[1] - cur[1]))
cur = p
prev_ctrl = None
elif kind == "Z":
emit("z", ())
cur = start
prev_ctrl = None
return "".join(out)
class Font:
"""Shapes text with HarfBuzz and draws its glyphs with fontTools."""
def __init__(self, path):
self.tt = TTFont(path)
self.upem = self.tt["head"].unitsPerEm
self.face = hb.Face(hb.Blob.from_file_path(str(path)))
self.glyph_order = self.tt.getGlyphOrder()
# Glyphs of the chip labels, each defined once and placed with <use>:
self.shared_glyphs = {}
def shape(self, text, size, wght, opsz, tracking=0.0):
"""Returns the glyphs of the text as (name, x, y) and the width of the text."""
font = hb.Font(self.face)
font.set_variations({"wght": wght, "opsz": opsz})
buf = hb.Buffer()
buf.add_str(text)
buf.guess_segment_properties()
hb.shape(font, buf, {"kern": True, "liga": True})
scale = size / self.upem
glyphs, x = [], 0.0
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
glyphs.append((self.glyph_order[info.codepoint], x + pos.x_offset * scale, pos.y_offset * scale))
x += pos.x_advance * scale + tracking * size
return glyphs, x - tracking * size
def width(self, text, size, wght, opsz):
return self.shape(text, size, wght, opsz)[1]
def path(self, text, x, y, size, wght, opsz, tracking=0.0):
"""Returns the path data of the text, starting at the baseline point (x, y)."""
glyphs, _ = self.shape(text, size, wght, opsz, tracking)
glyph_set = self.tt.getGlyphSet(location={"wght": wght, "opsz": opsz})
scale = size / self.upem
pen = RecordingPen(glyph_set)
for name, gx, gy in glyphs:
glyph_set[name].draw(TransformPen(pen, (scale, 0, 0, -scale, x + gx, y - gy)))
return compact_path(pen.ops)
def uses(self, text, x, y, size, wght, opsz):
"""Returns one <use> element per glyph of the text, registering new glyphs in shared_glyphs."""
glyphs, _ = self.shape(text, size, wght, opsz)
glyph_set = self.tt.getGlyphSet(location={"wght": wght, "opsz": opsz})
scale = size / self.upem
elements = []
for name, gx, gy in glyphs:
key = (name, size, wght, opsz)
if key not in self.shared_glyphs:
pen = RecordingPen(glyph_set)
glyph_set[name].draw(TransformPen(pen, (scale, 0, 0, -scale, 0, 0)))
self.shared_glyphs[key] = (f"g{len(self.shared_glyphs)}", compact_path(pen.ops))
glyph_id, d = self.shared_glyphs[key]
# Spaces have no outline:
if d:
elements.append(f'<use href="#{glyph_id}" x="{num(x + gx)}" y="{num(y - gy)}"/>')
return elements
def read_project_value(pattern, what):
match = re.search(pattern, APP_PROJECT.read_text(encoding="utf-8-sig"))
if match is None:
sys.exit(f"Could not find {what} in {APP_PROJECT}.")
return match.group(1)
def load_material_icons(names):
"""
Reads the SVG paths of Icons.Material.Filled.<name> from the MudBlazor assembly in the NuGet
cache, in the version and for the framework the app project uses.
"""
version = read_project_value(r'<PackageReference\s+Include="MudBlazor"\s+Version="([^"]+)"', "the MudBlazor version")
framework = read_project_value(r"<TargetFramework>([^<]+)</TargetFramework>", "the target framework")
packages = Path(os.environ.get("NUGET_PACKAGES", Path.home() / ".nuget" / "packages"))
assembly = packages / "mudblazor" / version / "lib" / framework / "MudBlazor.dll"
if not assembly.exists():
sys.exit(f"Could not find {assembly}. Build the app once, so NuGet restores MudBlazor {version}.")
tables = dnfile.dnPE(str(assembly)).net.mdtables
types = tables.TypeDef.rows
# Which type declares which field, and which type encloses which nested type:
field_owner = {}
for index, type_def in enumerate(types, start=1):
for field in type_def.FieldList:
field_owner[field.row_index] = index
enclosing = {n.NestedClass.row_index: str(n.EnclosingClass.row.TypeName) for n in tables.NestedClass.rows}
# The icons are string constants of the nested class Icons.Material.Filled:
icons = {}
for constant in tables.Constant.rows:
parent = constant.Parent
if parent.table.name != "Field":
continue
owner = field_owner.get(parent.row_index)
if owner is None or str(types[owner - 1].TypeName) != "Filled" or enclosing.get(owner) != "Material":
continue
name = str(tables.Field.rows[parent.row_index - 1].Name)
if name not in names:
continue
value = constant.Value
raw = value.value if hasattr(value, "value") else value
svg = raw.decode("utf-16le") if isinstance(raw, (bytes, bytearray)) else str(raw)
# Skip the invisible 24 x 24 frames some icons carry:
tags = re.findall(r"<path\b[^>]*>", svg)
icons[name] = [re.search(r'\bd="([^"]+)"', tag).group(1) for tag in tags if 'fill="none"' not in tag]
missing = sorted(set(names) - icons.keys())
if missing:
sys.exit(f"Icons.Material.Filled has no icon named {', '.join(missing)} in MudBlazor {version}.")
return icons
def load_font():
if not FONT_PATH.exists():
sys.exit(f"Could not find {FONT_PATH}. Download it first, see README.md:\ncurl -L -o \"{FONT_PATH}\" \"{FONT_URL}\"")
digest = hashlib.sha256(FONT_PATH.read_bytes()).hexdigest()
if digest != FONT_SHA256:
print(f"Warning: {FONT_PATH.name} is not the pinned version of Inter. The outlines may differ from the committed banner.", file=sys.stderr)
return Font(FONT_PATH)
def build_banner(font, icons):
lay = LAYOUT
out = []
a = out.append
a(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" aria-label="MindWork AI Studio">')
a('<!-- Text outlined from Inter (SIL Open Font License 1.1), icons from Material Icons via MudBlazor (Apache License 2.0). -->')
a('<title>MindWork AI Studio</title>')
a('<defs>')
a(f'<linearGradient id="bg" x1="0" y1="0" x2="{WIDTH}" y2="{HEIGHT}" gradientUnits="userSpaceOnUse">'
f'<stop offset="0" stop-color="#0f3b2c"/><stop offset=".6" stop-color="#1f6b45"/><stop offset="1" stop-color="#4a9a55"/></linearGradient>')
sun_x, sun_y, sun_r = lay["sun"]
a(f'<radialGradient id="sun" cx="{sun_x}" cy="{sun_y}" r="{sun_r}" gradientUnits="userSpaceOnUse">'
f'<stop offset="0" stop-color="#fdf0bf"/><stop offset=".14" stop-color="#f7d774"/>'
f'<stop offset=".2" stop-color="#f7d774" stop-opacity=".7"/><stop offset=".45" stop-color="#f7d774" stop-opacity=".22"/>'
f'<stop offset="1" stop-color="#f7d774" stop-opacity="0"/></radialGradient>')
a('</defs>')
a(f'<rect width="{WIDTH}" height="{HEIGHT}" rx="{CORNER_RADIUS}" fill="url(#bg)"/>')
a(f'<rect width="{WIDTH}" height="{HEIGHT}" rx="{CORNER_RADIUS}" fill="url(#sun)"/>')
# Wordmark on the left:
left = lay["left"]
a(f'<g id="wordmark" fill="{CREAM}">')
a(f'<path id="eyebrow" opacity=".85" d="{font.path(EYEBROW, left + 4, lay["eyebrow_y"], lay["eyebrow_size"], tracking=0.28, **EYEBROW_STYLE)}"/>')
a(f'<path id="name" d="{font.path(NAME, left, lay["name_y"], lay["name_size"], tracking=-0.02, **NAME_STYLE)}"/>')
a(f'<path id="tagline" opacity=".9" d="{font.path(TAGLINE, left + 2, lay["tag_y"], lay["tag_size"], **TEXT_STYLE)}"/>')
a('</g>')
# Chips on the right, two rows of four:
size, icon_size, pad, icon_gap = lay["chip_font"], lay["chip_icon"], lay["chip_pad"], lay["chip_icon_gap"]
chip_h, gap, row_gap = lay["chip_h"], lay["chip_gap"], lay["chip_row_gap"]
rows = [CHIPS[:4], CHIPS[4:]]
content_widths = [[icon_size + icon_gap + font.width(label, size, **TEXT_STYLE) for (_, label, _) in row] for row in rows]
columns = [max(content_widths[0][i], content_widths[1][i]) + 2 * pad for i in range(4)]
x0 = WIDTH - lay["right"] - (sum(columns) + 3 * gap)
top = lay["chip_cy"] - chip_h - row_gap / 2
a(f'<g id="chips" fill="{CREAM}">')
for r, row in enumerate(rows):
y = top + r * (chip_h + row_gap)
x = x0
for c, (chip_id, label, icon) in enumerate(row):
w = columns[c]
a(f'<g id="chip-{chip_id}">')
a(f'<rect x="{num(x)}" y="{num(y)}" width="{num(w)}" height="{chip_h}" rx="{chip_h / 2:g}" fill="{lay["chip_fill"]}" fill-opacity="{lay["chip_fill_op"]}" stroke="{CREAM}" stroke-opacity="{lay["chip_stroke_op"]}" stroke-width="2"/>')
# Icon and label centred in the chip:
content_x = x + (w - content_widths[r][c]) / 2
for d in icons[icon]:
a(f'<path transform="translate({num(content_x)} {num(y + (chip_h - icon_size) / 2)}) scale({icon_size / 24:g})" d="{d}"/>')
# Centre the cap height of Inter (0.727 em) vertically:
baseline = y + chip_h / 2 + 0.727 * size / 2
out.extend(font.uses(label, content_x + icon_size + icon_gap, baseline, size, **TEXT_STYLE))
a('</g>')
x += w + gap
a('</g>')
a('</svg>')
# The glyphs of the chip labels go into <defs>:
i = out.index('</defs>')
out[i:i] = [f'<path id="{glyph_id}" d="{d}"/>' for (glyph_id, d) in font.shared_glyphs.values() if d]
return "\n".join(out)
def main():
parser = argparse.ArgumentParser(description="Generates the banner of the start page and of the README.")
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT, help=f"where to write the SVG file (default: {DEFAULT_OUTPUT})")
args = parser.parse_args()
svg = build_banner(load_font(), load_material_icons({icon for (_, _, icon) in CHIPS}))
args.output.write_text(svg, encoding="utf-8")
print(f"Wrote {args.output} ({len(svg.encode())} bytes).")
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
dnfile==0.18.0
fonttools==4.66.1
uharfbuzz==0.56.3
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+496
View File
@@ -0,0 +1,496 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1000"
height="1000"
viewBox="0 0 200 200"
role="img"
aria-labelledby="title desc"
version="1.1"
id="svg66"
sodipodi:docname="AI_Studio_Logo.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
inkscape:export-filename="AI_Studio_Logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview66"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82519361"
inkscape:cx="238.12594"
inkscape:cy="544.72065"
inkscape:window-width="2560"
inkscape:window-height="1369"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g66" />
<title
id="title">AI Studio Logo</title>
<desc
id="desc">Grüne Waldlandschaft mit Sprechblase, drei Punkten, Wolke und warmer Sonne.</desc>
<defs
id="defs26">
<clipPath
id="artboard">
<rect
x="6"
y="8"
width="188"
height="185"
rx="39"
ry="39"
id="rect1" />
</clipPath>
<linearGradient
id="sky"
x1="18"
y1="180"
x2="181"
y2="17"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#397d6c"
id="stop1" />
<stop
offset=".53"
stop-color="#66aa83"
id="stop2" />
<stop
offset="1"
stop-color="#9dba69"
id="stop3" />
</linearGradient>
<radialGradient
id="sun"
cx="177"
cy="12"
r="58"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#f8dc62"
id="stop4" />
<stop
offset=".52"
stop-color="#e8c95c"
stop-opacity=".92"
id="stop5" />
<stop
offset="1"
stop-color="#d9bd57"
stop-opacity="0"
id="stop6" />
</radialGradient>
<linearGradient
id="bubble"
x1="43"
y1="48"
x2="97"
y2="115"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#eee6af"
id="stop7" />
<stop
offset=".42"
stop-color="#e7edb0"
id="stop8" />
<stop
offset="1"
stop-color="#e6eca8"
id="stop9" />
</linearGradient>
<linearGradient
id="bubbleTail"
x1="49"
y1="106"
x2="61"
y2="142"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#d6d99b"
id="stop10" />
<stop
offset="1"
stop-color="#c9c78d"
id="stop11" />
</linearGradient>
<linearGradient
id="cloud"
x1="133"
y1="121"
x2="136"
y2="166"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#f8e77b"
id="stop12" />
<stop
offset=".43"
stop-color="#e9dc70"
id="stop13" />
<stop
offset=".7"
stop-color="#8caf72"
id="stop14" />
<stop
offset="1"
stop-color="#77a786"
id="stop15" />
</linearGradient>
<linearGradient
id="hill"
x1="64"
y1="150"
x2="70"
y2="189"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#0c633c"
id="stop16" />
<stop
offset="1"
stop-color="#063b2f"
id="stop17" />
</linearGradient>
<linearGradient
id="hillLight"
x1="108"
y1="145"
x2="128"
y2="188"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#4f982f"
id="stop18" />
<stop
offset="1"
stop-color="#11673a"
id="stop19" />
</linearGradient>
<filter
id="softShadow"
x="-0.053877552"
y="-0.053333334"
width="1.1077551"
height="1.1066667">
<feGaussianBlur
stdDeviation="2.2"
id="feGaussianBlur19" />
</filter>
<filter
id="cloudShadow"
x="-0.084210526"
y="-0.10434783"
width="1.1684211"
height="1.2608696">
<feGaussianBlur
in="SourceAlpha"
stdDeviation="2"
result="blur"
id="feGaussianBlur20" />
<feOffset
dy="2.4"
result="off"
id="feOffset20" />
<feColorMatrix
in="off"
type="matrix"
values="0 0 0 0 0.12 0 0 0 0 0.28 0 0 0 0 0.22 0 0 0 .28 0"
id="feColorMatrix20" />
<feMerge
id="feMerge21">
<feMergeNode
id="feMergeNode20" />
<feMergeNode
in="SourceGraphic"
id="feMergeNode21" />
</feMerge>
</filter>
<g
id="pine-dark">
<path
d="M0 0 .9 31-.9 31Z"
fill="#153f35"
id="path21" />
<path
d="M0 1-5 12-2.2 11-7 19-3 17-9 27-2.5 24 0 31Z"
fill="#0b4436"
id="path22" />
<path
d="M0 1 4.5 12 2 11 6.5 19 2.8 17 8 27 2.4 24 0 31Z"
fill="#17543d"
id="path23" />
</g>
<g
id="pine-light">
<path
d="M0 0 1 32-1 32Z"
fill="#174b35"
id="path24" />
<path
d="M0 0-5 13-2.5 12-7 20-3 18-9 28-2.5 25 0 32Z"
fill="#176d3c"
id="path25" />
<path
d="M0 0 5 13 2.4 12 7 20 3 18 9 28 2.5 25 0 32Z"
fill="#5b9c3f"
id="path26" />
</g>
</defs>
<g
clip-path="url(#artboard)"
id="g66">
<!-- sky and late-afternoon glow -->
<rect
x="6"
y="8"
width="188"
height="185"
rx="39"
fill="url(#sky)"
id="rect26" />
<circle
cx="176"
cy="8"
r="58"
fill="url(#sun)"
id="circle26" />
<path
d="M132 8c-1 24 16 47 40 54 7 2 14 2 22 0V8Z"
fill="#f1cd5a"
opacity=".22"
id="path27" />
<!-- distant left mountain and tiny ridge details -->
<path
d="m29 58 10 33 17 25-18-17Z"
fill="#718d70"
opacity=".35"
id="path30" />
<!-- small lookout silhouette -->
<!-- shadow behind chat bubble -->
<path
d="M47 48h46c15 0 25 12 25 27v14c0 16-11 27-25 27H63c-2 7 1 17 6 29-11-11-21-20-28-28-13-3-21-13-21-28V73c0-15 11-27 27-27Z"
fill="#183f36"
opacity=".34"
filter="url(#softShadow)"
id="path37" />
<path
d="M45 50h47c14 0 23 11 23 25v16c0 14-10 25-23 25H60c-1 7 3 17 8 28-10-11-19-19-26-27-12-3-20-12-20-26V75c0-14 10-25 23-25Z"
fill="#b98370"
opacity=".64"
id="path38" />
<!-- main chat bubble -->
<path
d="M47 47h46c13 0 23 11 23 25v17c0 14-10 25-23 25H63c-3 7-1 17 4 27-10-10-18-19-25-27-11-3-19-12-19-25V73c0-15 10-26 24-26Z"
fill="url(#bubble)"
stroke="#f4edb5"
stroke-width=".8"
id="path39" />
<path
d="M42 114c6 1 13 1 21 0-3 7-1 17 4 27-10-10-18-19-25-27Z"
fill="url(#bubbleTail)"
id="path40" />
<path
d="M64 113h29c12 0 20-7 23-19-1 13-10 22-23 23H63Z"
fill="#9ca97a"
opacity=".2"
id="path41" />
<path
d="M25 68c5-13 13-20 27-21h12L28 95c-3-7-4-17-3-27Z"
fill="#f2d7aa"
opacity=".15"
id="path42" />
<!-- conversation dots -->
<circle
cx="45"
cy="84"
r="7"
fill="#23833f"
id="circle42"
style="fill:#268040;fill-opacity:1" />
<circle
cx="68"
cy="84"
r="7"
fill="#2b8b57"
id="circle43"
style="fill:#278040;fill-opacity:1" />
<circle
cx="90"
cy="84"
r="7"
fill="#4a9a66"
id="circle44"
style="fill:#268040;fill-opacity:1" />
<!-- tiny floating marks -->
<!-- small pale cloud above the main cloud -->
<path
d="m 127.25364,93.888603 h 26 c 0,-2 -3,-4 -6,-4 h -5 c 0,-4 -2,-7 -5,-7 -3,0 -5,3 -5,7 -3,0 -5,1 -5,4 z"
fill="#f5df8a"
id="path45" />
<path
d="m 133.25364,89.888603 c 0,-3 1,-5 3,-6 l -1,9 z"
fill="#dfc269"
opacity="0.55"
id="path46" />
<path
d="m 30.97828,36.962689 h 26 c 0,-2 -3,-4 -6,-4 h -5 c 0,-4 -2,-7 -5,-7 -3,0 -5,3 -5,7 -3,0 -5,1 -5,4 z"
fill="#f5df8a"
id="path45-8-2" />
<path
d="m 37.37828,32.562689 c 0,-3 1,-5 3,-6 l -1,9 z"
fill="#dfc269"
opacity="0.55"
id="path46-4" />
<!-- large cloud -->
<g
filter="url(#cloudShadow)"
id="g50">
<path
d="M104 140c0-6 5-11 11-11 2-5 6-8 11-8 5 0 9 3 11 7 3-4 7-6 12-5 7 1 12 7 12 14 0 6-3 10-7 14-4 4-9 10-17 16-7-7-12-12-17-15-10-1-16-5-16-12Z"
fill="url(#cloud)"
id="path47" />
<path
d="M106 141c11-2 22-2 32 2 8-5 15-5 22-3-1 5-3 8-6 11-4 4-9 10-17 16-7-7-12-12-17-15-8-1-13-4-14-11Z"
fill="#7ea86f"
opacity=".48"
id="path49" />
<path
d="M117 131c3-3 6-4 9-3l-10 8-6 2Z"
fill="#d1c35f"
opacity=".55"
id="path50" />
</g>
<!-- layered ground -->
<path
d="M42 169c15-13 30-20 46-21 17-1 31 6 43 18l-47 17Z"
fill="url(#hill)"
id="path51" />
<path
d="M76 169c9-12 19-20 31-21 13 1 26 9 36 24-22-8-42-9-67-3Z"
fill="url(#hillLight)"
id="path52" />
<path
d="M91 162c6-7 12-12 18-14 11 2 21 9 29 20-14-5-28-7-45-5Z"
fill="#529837"
opacity=".72"
id="path53" />
<path
d="M94 158c6-3 11-6 16-8m-12 11c7-5 13-8 19-10m-13 12c7-5 13-8 19-9m-15 11c7-4 13-7 19-8"
fill="none"
stroke="#9eb83e"
stroke-width=".75"
opacity=".74"
id="path54" />
<path
d="M102 153c5 2 10 5 14 9m-6-12c6 3 11 7 16 12m-8-10c6 3 11 7 15 11"
fill="none"
stroke="#247b37"
stroke-width=".65"
opacity=".85"
id="path55" />
<path
d="M6 181c20-12 39-20 60-22 24-2 45 4 66 10 22 6 40 8 62 16v8H6Z"
fill="#12613a"
id="path56" />
<path
d="M6 184c27-16 54-24 81-24 35 0 67 15 107 27v6H6Z"
fill="#126b3c"
opacity=".78"
id="path57" />
<path
d="M6 193c30-15 59-20 91-19 31 1 64 7 97 16v3Z"
fill="#075039"
opacity=".82"
id="path58" />
<!-- left forest -->
<g
transform="matrix(0.84,0,0,0.84,31.4,143.4)"
id="g58">
<use
href="#pine-dark"
id="use58" />
</g>
<g
transform="matrix(1.12,0,0,1.12,49,129)"
id="g59">
<use
href="#pine-dark"
id="use59" />
</g>
<g
transform="matrix(0.7,0,0,0.7,39,145.4)"
id="g60">
<use
href="#pine-dark"
id="use60" />
</g>
<g
transform="matrix(0.7,0,0,0.7,89.06539,127.87167)"
id="g60-5">
<use
href="#pine-dark"
id="use60-1" />
</g>
<g
transform="matrix(0.66,0,0,0.66,25.4,151.4)"
id="g61">
<use
href="#pine-light"
id="use61" />
</g>
<g
transform="matrix(0.5,0,0,0.5,19.2,159.8)"
id="g62">
<use
href="#pine-dark"
id="use62" />
</g>
<!-- right pines -->
<g
transform="matrix(1.35,0,0,1.35,170,135.8)"
id="g63">
<use
href="#pine-light"
id="use63" />
</g>
<g
transform="matrix(0.75,0,0,0.75,177,157.8)"
id="g64">
<use
href="#pine-light"
id="use64" />
</g>
<g
transform="matrix(0.75,0,0,0.75,82.453055,128.12775)"
id="g64-1">
<use
href="#pine-light"
id="use64-5" />
</g>
<g
transform="matrix(0.64,0,0,0.64,160.8,157.4)"
id="g65">
<use
href="#pine-dark"
id="use65" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.
Binary file not shown.