feat: portfolio-led redesign with Shootings umbrella, original logo and motion layer
37
AGENTS.md
@@ -7,7 +7,17 @@ no runtime dependencies. `src/` holds Python builders and the editorial content,
|
|||||||
`scripts/build-site.py` renders everything into the committed `site/` directory,
|
`scripts/build-site.py` renders everything into the committed `site/` directory,
|
||||||
and nginx serves `site/` from a container behind the shared VPS Traefik instance.
|
and nginx serves `site/` from a container behind the shared VPS Traefik instance.
|
||||||
|
|
||||||
`dennyschulz.de` is a **separate** site and must never be modified or linked from here.
|
`dennyschulz.de` is a **separate** site: nothing here may modify it, proxy it or
|
||||||
|
hotlink its assets. Linking out to it from the footer is an explicit operator
|
||||||
|
request and is the only permitted reference (see "Editing rules").
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
- `/` — hero, two pillars (**Shootings** = photo, **Videoprojekte** = video), portfolio
|
||||||
|
gallery, music video, mobiles Studio, process, facts.
|
||||||
|
- `/shootings/` — the umbrella for all photographic work: Businessfotografie,
|
||||||
|
Portrait & Model, Familien & Paare, Minishootings, plus the mobiles Studio.
|
||||||
|
`/fotografie` and `/fotografie/` are 301-redirected here by `nginx.conf`.
|
||||||
|
- `/video/`, `/social-media/`, `/projekte/` (music + free work + pro bono), `/ueber/`.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
- `python3 scripts/fetch-assets.py` — download pool images, Google Fonts subsets and
|
- `python3 scripts/fetch-assets.py` — download pool images, Google Fonts subsets and
|
||||||
@@ -27,10 +37,35 @@ and nginx serves `site/` from a container behind the shared VPS Traefik instance
|
|||||||
- Never invent facts. Prices, testimonials, client names, awards and biography dates
|
- Never invent facts. Prices, testimonials, client names, awards and biography dates
|
||||||
are intentionally absent; do not add them without verified information. Legal data
|
are intentionally absent; do not add them without verified information. Legal data
|
||||||
in `src/content.py:LEGAL` mirrors the operator's own Impressum (`§ 5 DDG`).
|
in `src/content.py:LEGAL` mirrors the operator's own Impressum (`§ 5 DDG`).
|
||||||
|
The portfolio must reflect the operator's real work (musicians, live shows, free
|
||||||
|
and artistic series, business shoots) — never invent trips, locations or jobs
|
||||||
|
(an earlier build captioned an image as a "Japanreise"; that was invented and must
|
||||||
|
not return).
|
||||||
- Keep the design language: paper/ink/rust/teal tokens from `src/theme.py`, rounded
|
- Keep the design language: paper/ink/rust/teal tokens from `src/theme.py`, rounded
|
||||||
cards, hairline dividers, Fraunces for display and Manrope for text.
|
cards, hairline dividers, Fraunces for display and Manrope for text.
|
||||||
- Images come from the operator's own pool through `src/images.json`. Only add an
|
- Images come from the operator's own pool through `src/images.json`. Only add an
|
||||||
entry if the operator holds the rights; always give a meaningful German `alt`.
|
entry if the operator holds the rights; always give a meaningful German `alt`.
|
||||||
|
`"source": "legacy"` marks images fetched from the operator's own site
|
||||||
|
(`dennyschulz.de/legacy/<file>`) — they are written into `assets-src/images/` and
|
||||||
|
never hotlinked at runtime. `alt` texts must come from the operator's own captions.
|
||||||
|
- The operator's original logo lives in `assets-src/brand/logo.png`; the build derives
|
||||||
|
`logo-160.png`, the favicons, the apple-touch icon and `favicon.svg` from it. Never
|
||||||
|
redraw or guess a replacement mark.
|
||||||
|
- The footer's "Netzwerk" column links to `https://www.dennyschulz.de/` and
|
||||||
|
`https://dennyapp.de/` because the operator asked for it; these are the only
|
||||||
|
external links allowed and they must keep `target="_blank" rel="noopener"`.
|
||||||
|
Nothing else may reference `dennyschulz.de` (no assets, no fetches, no rewriting).
|
||||||
|
- Animations are progressive enhancement: they live in CSS plus the guarded
|
||||||
|
`site.js` block, every page carries the `<noscript>` reveal fallback, and the
|
||||||
|
`prefers-reduced-motion` block must stay the **last** rule in `src/theme.py` so it
|
||||||
|
wins over the motion definitions.
|
||||||
|
- Never hide the element you observe: Chromium folds a target's own `clip-path` into
|
||||||
|
the `IntersectionObserver` geometry, so a closed mask never reports as visible and
|
||||||
|
the content stays hidden forever. `.reveal--mask` therefore clips its children.
|
||||||
|
- Never nest `<a>` inside `<a>` (or any other invalid nesting). The parser closes the
|
||||||
|
outer anchor and the surrounding grid falls apart — that is what happened to the
|
||||||
|
two homepage pillars. Cards with their own links use the stretched-link pattern
|
||||||
|
(`.pillar__go::after`); `tests/test_site.py` guards this for every page.
|
||||||
- Two-space indentation, `from __future__ import annotations`, type hints on public
|
- Two-space indentation, `from __future__ import annotations`, type hints on public
|
||||||
functions, no new third-party Python packages (Pillow is the only dependency).
|
functions, no new third-party Python packages (Pillow is the only dependency).
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ server {
|
|||||||
location = /datenschutz.html { return 301 /datenschutz/; }
|
location = /datenschutz.html { return 301 /datenschutz/; }
|
||||||
location = /kontakt.html { return 301 /kontakt/; }
|
location = /kontakt.html { return 301 /kontakt/; }
|
||||||
|
|
||||||
|
# Shootings is the umbrella term for the photographic work; the old
|
||||||
|
# /fotografie/ URL stays reachable and points to the new page.
|
||||||
|
# (/fotografie/index.html is already canonicalised to /fotografie/ above.)
|
||||||
|
location = /fotografie { return 301 /shootings/; }
|
||||||
|
location = /fotografie/ { return 301 /shootings/; }
|
||||||
|
location = /fotografie.html { return 301 /shootings/; }
|
||||||
|
|
||||||
location ^~ /.well-known/ {
|
location ^~ /.well-known/ {
|
||||||
allow all;
|
allow all;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ Steps
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@@ -27,6 +29,7 @@ SITE = ROOT / "site"
|
|||||||
IMG_OUT = SITE / "assets" / "img"
|
IMG_OUT = SITE / "assets" / "img"
|
||||||
FONT_OUT = SITE / "assets" / "fonts"
|
FONT_OUT = SITE / "assets" / "fonts"
|
||||||
TTF = ASSETS / "fonts-ttf"
|
TTF = ASSETS / "fonts-ttf"
|
||||||
|
LOGO_SRC = ASSETS / "brand" / "logo.png"
|
||||||
|
|
||||||
sys.path.insert(0, str(SRC))
|
sys.path.insert(0, str(SRC))
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ import pages # noqa: E402
|
|||||||
from content import LEGAL, SITE as SITE_META, VIDEO # noqa: E402
|
from content import LEGAL, SITE as SITE_META, VIDEO # noqa: E402
|
||||||
from theme import CSS, TOKENS # noqa: E402
|
from theme import CSS, TOKENS # noqa: E402
|
||||||
|
|
||||||
WIDTHS = (480, 768, 1200, 1600, 2000)
|
WIDTHS = (480, 768, 1200, 1600)
|
||||||
WEBP_QUALITY = 84
|
WEBP_QUALITY = 84
|
||||||
|
|
||||||
INK = (23, 20, 15)
|
INK = (23, 20, 15)
|
||||||
@@ -44,16 +47,16 @@ ACCENT = (192, 67, 28)
|
|||||||
|
|
||||||
# OpenGraph image per route, built from the page's own key visual.
|
# OpenGraph image per route, built from the page's own key visual.
|
||||||
ROUTE_OG = {
|
ROUTE_OG = {
|
||||||
"/": ("hero-studio", "og-desfoto.jpg", "Bilder und Filme, die nicht beliebig aussehen."),
|
"/": ("music-vocal-blue", "og-desfoto.jpg", "Bilder und Filme, die nicht beliebig aussehen."),
|
||||||
"/fotografie/": ("editorial-earring", "og-fotografie.jpg", "Fotografie aus Neumünster"),
|
"/shootings/": ("business-portrait-studio", "og-shootings.jpg", "Shootings aus Neumünster"),
|
||||||
"/businessfotografie/": ("business-portrait-studio", "og-businessfotografie.jpg", "Businessfotografie"),
|
"/businessfotografie/": ("business-portrait-studio", "og-businessfotografie.jpg", "Businessfotografie"),
|
||||||
"/portrait-und-model/": ("editorial-colour", "og-portrait.jpg", "Portrait & Model"),
|
"/portrait-und-model/": ("editorial-colour", "og-portrait.jpg", "Portrait & Model"),
|
||||||
"/familien-und-paare/": ("people-joy", "og-familien.jpg", "Familien & Paare"),
|
"/familien-und-paare/": ("people-joy", "og-familien.jpg", "Familien & Paare"),
|
||||||
"/minishootings/": ("portrait-natural", "og-minishootings.jpg", "Minishootings"),
|
"/minishootings/": ("portrait-natural", "og-minishootings.jpg", "Minishootings"),
|
||||||
"/video/": ("studio-lens", "og-video.jpg", "Videoprojekte"),
|
"/video/": ("music-guitar-silhouette", "og-video.jpg", "Videoprojekte"),
|
||||||
"/social-media/": ("event-motion", "og-social-media.jpg", "Social-Media-Inhalte"),
|
"/social-media/": ("event-motion", "og-social-media.jpg", "Social-Media-Inhalte"),
|
||||||
"/projekte/": ("travel-norway-pano", "og-projekte.jpg", "Eigene Projekte"),
|
"/projekte/": ("free-mountain-view", "og-projekte.jpg", "Eigene Projekte"),
|
||||||
"/ueber/": ("business-portrait-dark", "og-ueber.jpg", "Über desfoto"),
|
"/ueber/": ("studio-setup", "og-ueber.jpg", "Über desfoto"),
|
||||||
"/kontakt/": ("event-venue", "og-kontakt.jpg", "Kontakt"),
|
"/kontakt/": ("event-venue", "og-kontakt.jpg", "Kontakt"),
|
||||||
"/impressum/": ("gear-cases", "og-impressum.jpg", "Impressum"),
|
"/impressum/": ("gear-cases", "og-impressum.jpg", "Impressum"),
|
||||||
"/datenschutz/": ("event-venue", "og-datenschutz.jpg", "Datenschutz"),
|
"/datenschutz/": ("event-venue", "og-datenschutz.jpg", "Datenschutz"),
|
||||||
@@ -159,59 +162,38 @@ def _font(name: str, size: int) -> ImageFont.FreeTypeFont:
|
|||||||
return ImageFont.truetype(str(TTF / name), size)
|
return ImageFont.truetype(str(TTF / name), size)
|
||||||
|
|
||||||
|
|
||||||
def draw_mark(size: int, background: bool = True) -> Image.Image:
|
def load_logo() -> Image.Image:
|
||||||
"""The desfoto aperture mark, drawn deterministically with PIL."""
|
"""The operator's own logo (as published on dennyschulz.de)."""
|
||||||
scale = 8
|
if not LOGO_SRC.exists():
|
||||||
s = size * scale
|
raise SystemExit(f"missing brand asset {LOGO_SRC} - run scripts/fetch-assets.py")
|
||||||
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
|
return Image.open(LOGO_SRC).convert("RGBA")
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
if background:
|
|
||||||
d.rounded_rectangle((0, 0, s - 1, s - 1), radius=int(s * 0.22), fill=INK + (255,))
|
|
||||||
cx = cy = s / 2
|
|
||||||
ring_w = max(2, int(s * 0.055))
|
|
||||||
d.ellipse(
|
|
||||||
(cx - s * 0.27, cy - s * 0.27, cx + s * 0.27, cy + s * 0.27),
|
|
||||||
outline=PAPER + (255,),
|
|
||||||
width=ring_w,
|
|
||||||
)
|
|
||||||
d.ellipse(
|
|
||||||
(cx - s * 0.105, cy - s * 0.105, cx + s * 0.105, cy + s * 0.105),
|
|
||||||
outline=PAPER + (255,),
|
|
||||||
width=ring_w,
|
|
||||||
)
|
|
||||||
blade_w = max(2, int(s * 0.058))
|
|
||||||
import math
|
|
||||||
|
|
||||||
for angle in (270, 30, 150):
|
|
||||||
rad = math.radians(angle)
|
|
||||||
x1 = cx + math.cos(rad) * s * 0.27
|
|
||||||
y1 = cy + math.sin(rad) * s * 0.27
|
|
||||||
x2 = cx + math.cos(rad) * s * 0.45
|
|
||||||
y2 = cy + math.sin(rad) * s * 0.45
|
|
||||||
d.line((x1, y1, x2, y2), fill=ACCENT + (255,), width=blade_w)
|
|
||||||
for (px, py) in ((x1, y1), (x2, y2)):
|
|
||||||
r = blade_w / 2
|
|
||||||
d.ellipse((px - r, py - r, px + r, py + r), fill=ACCENT + (255,))
|
|
||||||
return img.resize((size, size), Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
|
|
||||||
def build_brand_assets(table: dict[str, dict]) -> None:
|
def build_brand_assets(table: dict[str, dict]) -> None:
|
||||||
|
logo = load_logo()
|
||||||
|
# 1:1 copy of the operator's original logo for the header/footer mark.
|
||||||
|
logo.save(IMG_OUT / "logo-160.png", "PNG", optimize=True)
|
||||||
|
logo.resize((180, 180), Image.Resampling.LANCZOS).save(
|
||||||
|
IMG_OUT / "apple-touch-icon.png", "PNG", optimize=True
|
||||||
|
)
|
||||||
|
logo.resize((32, 32), Image.Resampling.LANCZOS).save(
|
||||||
|
IMG_OUT / "favicon-32.png", "PNG", optimize=True
|
||||||
|
)
|
||||||
|
logo.resize((16, 16), Image.Resampling.LANCZOS).save(
|
||||||
|
IMG_OUT / "favicon-16.png", "PNG", optimize=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Self-contained SVG favicon; the operator's own site uses the same trick.
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
logo.save(buffer, "PNG", optimize=True)
|
||||||
|
encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
|
||||||
(SITE / "favicon.svg").write_text(
|
(SITE / "favicon.svg").write_text(
|
||||||
"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="desfoto">
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160" role="img" aria-label="desfoto">\n'
|
||||||
<rect width="64" height="64" rx="14" fill="#17140F"/>
|
f' <image width="160" height="160" href="data:image/png;base64,{encoded}"/>\n'
|
||||||
<circle cx="32" cy="32" r="17" fill="none" stroke="#F6F2EA" stroke-width="3.4"/>
|
"</svg>\n",
|
||||||
<circle cx="32" cy="32" r="6.6" fill="none" stroke="#F6F2EA" stroke-width="2.6"/>
|
|
||||||
<g stroke="#C0431C" stroke-width="3.6" stroke-linecap="round">
|
|
||||||
<path d="M32 15V6"/><path d="M46.7 40.5 54.4 45"/><path d="M17.3 40.5 9.6 45"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
""",
|
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
draw_mark(180).save(IMG_OUT / "apple-touch-icon.png")
|
print(" [brand] logo-160.png, favicon.svg, apple-touch-icon.png, favicon-32.png")
|
||||||
draw_mark(64).save(IMG_OUT / "favicon-32.png")
|
|
||||||
draw_mark(64).resize((16, 16), Image.Resampling.LANCZOS).save(IMG_OUT / "favicon-16.png")
|
|
||||||
print(" [brand] favicon.svg, apple-touch-icon.png, favicon-32.png")
|
|
||||||
|
|
||||||
og_done = set()
|
og_done = set()
|
||||||
for slug, filename, headline in ROUTE_OG.values():
|
for slug, filename, headline in ROUTE_OG.values():
|
||||||
@@ -219,6 +201,11 @@ def build_brand_assets(table: dict[str, dict]) -> None:
|
|||||||
continue
|
continue
|
||||||
og_done.add(filename)
|
og_done.add(filename)
|
||||||
build_og(table, slug, filename, headline)
|
build_og(table, slug, filename, headline)
|
||||||
|
# A renamed route would otherwise leave its old card behind forever.
|
||||||
|
for stale in sorted(IMG_OUT.glob("og-*.jpg")):
|
||||||
|
if stale.name not in og_done:
|
||||||
|
stale.unlink()
|
||||||
|
print(f" [brand] removed stale {stale.name}")
|
||||||
|
|
||||||
|
|
||||||
def build_og(table: dict[str, dict], slug: str, filename: str, headline: str) -> None:
|
def build_og(table: dict[str, dict], slug: str, filename: str, headline: str) -> None:
|
||||||
@@ -249,8 +236,10 @@ def build_og(table: dict[str, dict], slug: str, filename: str, headline: str) ->
|
|||||||
brand_font = _font("fraunces-600.ttf", 92)
|
brand_font = _font("fraunces-600.ttf", 92)
|
||||||
head_font = _font("fraunces-400.ttf", 54)
|
head_font = _font("fraunces-400.ttf", 54)
|
||||||
small_font = _font("manrope-600.ttf", 26)
|
small_font = _font("manrope-600.ttf", 26)
|
||||||
d.text((72, 74), "desfoto", font=brand_font, fill=PAPER)
|
mark = load_logo().resize((102, 102), Image.Resampling.LANCZOS)
|
||||||
d.text((76, 186), "FOTOGRAFIE & VIDEO · NEUMÜNSTER", font=small_font, fill=(230, 161, 132))
|
source.paste(mark, (76, 62), mark)
|
||||||
|
d.text((198, 72), "desfoto", font=brand_font, fill=PAPER)
|
||||||
|
d.text((202, 186), "FOTOGRAFIE & VIDEO · NEUMÜNSTER", font=small_font, fill=(230, 161, 132))
|
||||||
lines = wrap_text(headline, head_font, target_w - 150)
|
lines = wrap_text(headline, head_font, target_w - 150)
|
||||||
y = target_h - 90 - len(lines) * 64
|
y = target_h - 90 - len(lines) * 64
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@@ -421,22 +410,108 @@ def build_script() -> None:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Soft reveal on scroll, disabled for reduced motion */
|
/* Motion layer: reveals, scroll progress, parallax and pointer spotlights.
|
||||||
|
Everything degrades to a static page and is disabled for reduced motion. */
|
||||||
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
/* Anything inside [data-stagger] reveals one item after another. */
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('[data-stagger]'), function (group) {
|
||||||
|
var step = parseFloat(group.getAttribute('data-stagger')) || 0.08;
|
||||||
|
Array.prototype.forEach.call(group.children, function (child, index) {
|
||||||
|
if (!child.classList.contains('reveal')) {
|
||||||
|
child.classList.add('reveal');
|
||||||
|
}
|
||||||
|
child.style.setProperty('--d', (index * step).toFixed(2) + 's');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var items = document.querySelectorAll('.reveal');
|
var items = document.querySelectorAll('.reveal');
|
||||||
|
var revealAll = function () {
|
||||||
|
Array.prototype.forEach.call(items, function (item) { item.classList.add('is-in'); });
|
||||||
|
};
|
||||||
if (reduce || !('IntersectionObserver' in window)) {
|
if (reduce || !('IntersectionObserver' in window)) {
|
||||||
items.forEach(function (item) { item.classList.add('is-in'); });
|
revealAll();
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
|
var fired = 0;
|
||||||
var observer = new IntersectionObserver(function (entries) {
|
var observer = new IntersectionObserver(function (entries) {
|
||||||
entries.forEach(function (entry) {
|
entries.forEach(function (entry) {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
|
fired = 1;
|
||||||
entry.target.classList.add('is-in');
|
entry.target.classList.add('is-in');
|
||||||
observer.unobserve(entry.target);
|
observer.unobserve(entry.target);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, { rootMargin: '0px 0px -10% 0px', threshold: 0.05 });
|
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.06 });
|
||||||
items.forEach(function (item) { observer.observe(item); });
|
Array.prototype.forEach.call(items, function (item) { observer.observe(item); });
|
||||||
|
/* Safety net: if the browser never reports a box that is on screen, drop the
|
||||||
|
effect entirely instead of leaving content hidden. */
|
||||||
|
window.setTimeout(function () {
|
||||||
|
if (fired) { return; }
|
||||||
|
var onScreen = false;
|
||||||
|
Array.prototype.forEach.call(items, function (item) {
|
||||||
|
var box = item.getBoundingClientRect();
|
||||||
|
if (box.top < window.innerHeight && box.bottom > 0) { onScreen = true; }
|
||||||
|
});
|
||||||
|
if (onScreen) {
|
||||||
|
observer.disconnect();
|
||||||
|
revealAll();
|
||||||
}
|
}
|
||||||
|
}, 1600);
|
||||||
|
} catch (error) {
|
||||||
|
revealAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reduce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = document.querySelector('[data-progress]');
|
||||||
|
var parallax = Array.prototype.slice.call(document.querySelectorAll('[data-parallax]'));
|
||||||
|
var ticking = false;
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
var doc = document.documentElement;
|
||||||
|
var max = doc.scrollHeight - window.innerHeight;
|
||||||
|
var ratio = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
|
||||||
|
if (progress) {
|
||||||
|
progress.style.setProperty('--scroll', ratio.toFixed(4));
|
||||||
|
}
|
||||||
|
if (parallax.length) {
|
||||||
|
var view = window.innerHeight;
|
||||||
|
parallax.forEach(function (element) {
|
||||||
|
var rect = element.getBoundingClientRect();
|
||||||
|
if (rect.bottom < -240 || rect.top > view + 240) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var speed = parseFloat(element.getAttribute('data-parallax')) || 0.12;
|
||||||
|
var offset = (rect.top + rect.height / 2 - view / 2) * speed;
|
||||||
|
element.style.setProperty('--py', offset.toFixed(1) + 'px');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ticking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestScroll() {
|
||||||
|
if (!ticking) {
|
||||||
|
ticking = true;
|
||||||
|
window.requestAnimationFrame(onScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', requestScroll, { passive: true });
|
||||||
|
window.addEventListener('resize', requestScroll);
|
||||||
|
onScroll();
|
||||||
|
|
||||||
|
/* Pointer-following highlight on the media cards. */
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.spot'), function (card) {
|
||||||
|
card.addEventListener('pointermove', function (event) {
|
||||||
|
var rect = card.getBoundingClientRect();
|
||||||
|
card.style.setProperty('--mx', ((event.clientX - rect.left) / rect.width * 100).toFixed(1) + '%');
|
||||||
|
card.style.setProperty('--my', ((event.clientY - rect.top) / rect.height * 100).toFixed(1) + '%');
|
||||||
|
});
|
||||||
|
});
|
||||||
}());
|
}());
|
||||||
""",
|
""",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ Sources
|
|||||||
We pull the pre-derived `web` variant (long edge 2560 px) instead of the
|
We pull the pre-derived `web` variant (long edge 2560 px) instead of the
|
||||||
20-60 MB camera originals. Only assets we are allowed to publish as the
|
20-60 MB camera originals. Only assets we are allowed to publish as the
|
||||||
rights holder are listed in src/images.json.
|
rights holder are listed in src/images.json.
|
||||||
|
* Own portfolio files of the existing site (www.dennyschulz.de/legacy/<file>).
|
||||||
|
These carry the concert, band and free-work photographs that are missing from
|
||||||
|
the pool; they are fetched as-is and downscaled by the build. Also used for
|
||||||
|
the operator's original logo (www.dennyschulz.de/logo.png).
|
||||||
* Google Fonts (open licensed, OFL) - downloaded once, then self-hosted.
|
* Google Fonts (open licensed, OFL) - downloaded once, then self-hosted.
|
||||||
Licences are written to docs/.
|
Licences are written to docs/.
|
||||||
* YouTube poster frame for the own music video "Thjodroerir - Skogamor".
|
* YouTube poster frame for the own music video "Thjodroerir - Skogamor".
|
||||||
@@ -26,10 +30,13 @@ from pathlib import Path
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
SRC = ROOT / "assets-src"
|
SRC = ROOT / "assets-src"
|
||||||
IMG_OUT = SRC / "images"
|
IMG_OUT = SRC / "images"
|
||||||
|
BRAND_OUT = SRC / "brand"
|
||||||
FONT_OUT = SRC / "fonts"
|
FONT_OUT = SRC / "fonts"
|
||||||
TTF_OUT = SRC / "fonts-ttf"
|
TTF_OUT = SRC / "fonts-ttf"
|
||||||
DOCS = ROOT / "docs"
|
DOCS = ROOT / "docs"
|
||||||
|
|
||||||
|
LOGO_URL = "https://www.dennyschulz.de/logo.png"
|
||||||
|
|
||||||
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"
|
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"
|
||||||
# Old Safari advertises truetype-only support, so Google serves real .ttf
|
# Old Safari advertises truetype-only support, so Google serves real .ttf
|
||||||
# files (the MSIE 6 UA returns EOT, which PIL/FreeType cannot read).
|
# files (the MSIE 6 UA returns EOT, which PIL/FreeType cannot read).
|
||||||
@@ -81,22 +88,26 @@ def try_fetch(url: str, *, ua: str = UA) -> bytes | None:
|
|||||||
def fetch_images() -> None:
|
def fetch_images() -> None:
|
||||||
manifest = json.loads((ROOT / "src" / "images.json").read_text(encoding="utf-8"))
|
manifest = json.loads((ROOT / "src" / "images.json").read_text(encoding="utf-8"))
|
||||||
base = manifest["pool_base"].rstrip("/")
|
base = manifest["pool_base"].rstrip("/")
|
||||||
|
legacy_base = manifest["legacy_base"].rstrip("/")
|
||||||
|
suffixes = (".webp", ".src", ".jpg", ".jpeg", ".png")
|
||||||
ok = skipped = 0
|
ok = skipped = 0
|
||||||
for entry in manifest["images"]:
|
for entry in manifest["images"]:
|
||||||
slug, raw = entry["slug"], entry["raw"]
|
slug, raw = entry["slug"], entry["raw"]
|
||||||
rel = raw.replace("\\", "/")
|
rel = raw.replace("\\", "/")
|
||||||
target = IMG_OUT / f"{slug}.webp"
|
if any((IMG_OUT / f"{slug}{suffix}").exists() for suffix in suffixes):
|
||||||
source = IMG_OUT / f"{slug}.src"
|
|
||||||
if target.exists() or source.exists():
|
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
if entry.get("source") == "legacy":
|
||||||
|
data = fetch(f"{legacy_base}/{rel}")
|
||||||
|
(IMG_OUT / f"{slug}.jpg").write_bytes(data)
|
||||||
|
else:
|
||||||
data = try_fetch(f"{base}/_derived/web/{rel}.webp")
|
data = try_fetch(f"{base}/_derived/web/{rel}.webp")
|
||||||
if data is None:
|
if data is None:
|
||||||
print(f" [!] web variant missing, falling back to original: {rel}")
|
print(f" [!] web variant missing, falling back to original: {rel}")
|
||||||
data = fetch(f"{base}/{rel}")
|
data = fetch(f"{base}/{rel}")
|
||||||
source.write_bytes(data)
|
(IMG_OUT / f"{slug}.src").write_bytes(data)
|
||||||
else:
|
else:
|
||||||
target.write_bytes(data)
|
(IMG_OUT / f"{slug}.webp").write_bytes(data)
|
||||||
ok += 1
|
ok += 1
|
||||||
print(f" [ok] {slug} ({len(data) / 1024:.0f} KiB)")
|
print(f" [ok] {slug} ({len(data) / 1024:.0f} KiB)")
|
||||||
print(f"images: {ok} fetched, {skipped} already present")
|
print(f"images: {ok} fetched, {skipped} already present")
|
||||||
@@ -110,6 +121,18 @@ def fetch_images() -> None:
|
|||||||
print(f" [ok] video poster ({len(data) / 1024:.0f} KiB)")
|
print(f" [ok] video poster ({len(data) / 1024:.0f} KiB)")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_brand() -> None:
|
||||||
|
"""The operator's original logo (used for the brand mark and all icons)."""
|
||||||
|
BRAND_OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = BRAND_OUT / "logo.png"
|
||||||
|
if target.exists():
|
||||||
|
print(f"brand: logo already present ({target.stat().st_size / 1024:.0f} KiB)")
|
||||||
|
return
|
||||||
|
data = fetch(LOGO_URL)
|
||||||
|
target.write_bytes(data)
|
||||||
|
print(f" [ok] logo ({len(data) / 1024:.0f} KiB)")
|
||||||
|
|
||||||
|
|
||||||
def fetch_fonts() -> None:
|
def fetch_fonts() -> None:
|
||||||
FONT_OUT.mkdir(parents=True, exist_ok=True)
|
FONT_OUT.mkdir(parents=True, exist_ok=True)
|
||||||
for family, spec in FAMILIES.items():
|
for family, spec in FAMILIES.items():
|
||||||
@@ -160,8 +183,10 @@ def fetch_fonts() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
for path in (IMG_OUT, FONT_OUT, DOCS):
|
for path in (IMG_OUT, BRAND_OUT, FONT_OUT, DOCS):
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
print("== brand ==")
|
||||||
|
fetch_brand()
|
||||||
print("== images ==")
|
print("== images ==")
|
||||||
fetch_images()
|
fetch_images()
|
||||||
print("== fonts ==")
|
print("== fonts ==")
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<link rel="stylesheet" href="/assets/fonts.css">
|
<link rel="stylesheet" href="/assets/fonts.css">
|
||||||
<link rel="stylesheet" href="/assets/site.css">
|
<link rel="stylesheet" href="/assets/site.css">
|
||||||
|
<noscript><style>.reveal,.reveal--mask{opacity:1;transform:none;clip-path:none}.reveal--mask>*{clip-path:none}.hero__title .w>span,.hero__sub,.hero__actions,.hero__facts{opacity:1;transform:none}</style></noscript>
|
||||||
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@@ -35,20 +36,15 @@
|
|||||||
<header class="masthead">
|
<header class="masthead">
|
||||||
<div class="shell masthead__inner">
|
<div class="shell masthead__inner">
|
||||||
<a class="brand" href="/" aria-label="desfoto Startseite">
|
<a class="brand" href="/" aria-label="desfoto Startseite">
|
||||||
<svg class="brand__mark" viewBox="0 0 40 40" role="img" aria-label="desfoto Bildmarke">
|
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
|
||||||
<circle class="ring" cx="20" cy="20" r="13"/>
|
|
||||||
<circle class="ring" cx="20" cy="20" r="5.2" stroke-width="2"/>
|
|
||||||
<path class="blade" d="M20 7 20 1.6"/>
|
|
||||||
<path class="blade" d="M31.3 26.5 36 29.2"/>
|
|
||||||
<path class="blade" d="M8.7 26.5 4 29.2"/>
|
|
||||||
</svg>
|
|
||||||
<span class="brand__text"><span>desfoto</span><small>Fotografie & Video</small></span>
|
<span class="brand__text"><span>desfoto</span><small>Fotografie & Video</small></span>
|
||||||
</a>
|
</a>
|
||||||
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
|
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg> Menü
|
||||||
</button>
|
</button>
|
||||||
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/fotografie/">Fotografie<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait & Model</a><a href="/familien-und-paare/">Familien & Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/social-media/">Social Media</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
|
<nav class="nav" id="site-nav" aria-label="Hauptnavigation"><div class="nav__item"><a class="nav__link" href="/shootings/">Shootings<svg class="nav__caret" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg></a><div class="nav__sub"><a href="/businessfotografie/">Businessfotografie</a><a href="/portrait-und-model/">Portrait & Model</a><a href="/familien-und-paare/">Familien & Paare</a><a href="/minishootings/">Minishootings</a></div></div><a class="nav__link" href="/video/">Video</a><a class="nav__link" href="/projekte/">Projekte</a><a class="nav__link" href="/ueber/">Über</a><a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a></nav>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
|
||||||
</header>
|
</header>
|
||||||
<main id="main"><section class="section">
|
<main id="main"><section class="section">
|
||||||
<div class="shell center-page">
|
<div class="shell center-page">
|
||||||
@@ -70,20 +66,15 @@
|
|||||||
<div class="footer__grid">
|
<div class="footer__grid">
|
||||||
<div class="footer__brand">
|
<div class="footer__brand">
|
||||||
<span class="brand">
|
<span class="brand">
|
||||||
<svg class="brand__mark" viewBox="0 0 40 40" role="img" aria-label="desfoto Bildmarke">
|
<span class="brand__mark"><img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async"></span>
|
||||||
<circle class="ring" cx="20" cy="20" r="13"/>
|
|
||||||
<circle class="ring" cx="20" cy="20" r="5.2" stroke-width="2"/>
|
|
||||||
<path class="blade" d="M20 7 20 1.6"/>
|
|
||||||
<path class="blade" d="M31.3 26.5 36 29.2"/>
|
|
||||||
<path class="blade" d="M8.7 26.5 4 29.2"/>
|
|
||||||
</svg>
|
|
||||||
<span class="brand__text"><span>desfoto</span><small>Fotografie & Video</small></span>
|
<span class="brand__text"><span>desfoto</span><small>Fotografie & Video</small></span>
|
||||||
</span>
|
</span>
|
||||||
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
|
<p class="footer__claim">Fotografie und Video aus Neumünster. Ein Angebot von Denny Schulz Fotografie.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2>Fotografie</h2>
|
<h2>Shootings</h2>
|
||||||
<div class="footer__links">
|
<div class="footer__links">
|
||||||
|
<a href="/shootings/">Überblick Shootings</a>
|
||||||
<a href="/businessfotografie/">Businessfotografie</a>
|
<a href="/businessfotografie/">Businessfotografie</a>
|
||||||
<a href="/portrait-und-model/">Portrait & Model</a>
|
<a href="/portrait-und-model/">Portrait & Model</a>
|
||||||
<a href="/familien-und-paare/">Familien & Paare</a>
|
<a href="/familien-und-paare/">Familien & Paare</a>
|
||||||
@@ -91,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2>Weitere Leistungen</h2>
|
<h2>Video & mehr</h2>
|
||||||
<div class="footer__links">
|
<div class="footer__links">
|
||||||
<a href="/video/">Videoprojekte</a>
|
<a href="/video/">Videoprojekte</a>
|
||||||
<a href="/social-media/">Social Media</a>
|
<a href="/social-media/">Social Media</a>
|
||||||
@@ -108,6 +99,10 @@
|
|||||||
<span class="muted">Neumünster · Schleswig-Holstein</span>
|
<span class="muted">Neumünster · Schleswig-Holstein</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Netzwerk</h2>
|
||||||
|
<div class="footer__links"><a class="footer__ext" href="https://www.dennyschulz.de/" target="_blank" rel="noopener">dennyschulz.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.</small><a class="footer__ext" href="https://dennyapp.de/" target="_blank" rel="noopener">dennyapp.de <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg></a><small>Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.</small></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer__bottom">
|
<div class="footer__bottom">
|
||||||
<nav aria-label="Rechtliches">
|
<nav aria-label="Rechtliches">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 54 KiB |
BIN
site/assets/img/band-extinct-1200.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
site/assets/img/band-extinct-1600.webp
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
site/assets/img/band-extinct-480.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
site/assets/img/band-extinct-768.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
site/assets/img/band-sagenbringer-1200.webp
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
site/assets/img/band-sagenbringer-1600.webp
Normal file
|
After Width: | Height: | Size: 411 KiB |
BIN
site/assets/img/band-sagenbringer-480.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
site/assets/img/band-sagenbringer-768.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 763 B After Width: | Height: | Size: 927 B |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
site/assets/img/free-arch-garden-1200.webp
Normal file
|
After Width: | Height: | Size: 363 KiB |
BIN
site/assets/img/free-arch-garden-1600.webp
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
site/assets/img/free-arch-garden-480.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
site/assets/img/free-arch-garden-768.webp
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
site/assets/img/free-cat-1200.webp
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
site/assets/img/free-cat-1600.webp
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
site/assets/img/free-cat-480.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
site/assets/img/free-cat-768.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
site/assets/img/free-fjord-1200.webp
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
site/assets/img/free-fjord-1600.webp
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
site/assets/img/free-fjord-480.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
site/assets/img/free-fjord-768.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
site/assets/img/free-fog-forest-1200.webp
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
site/assets/img/free-fog-forest-1600.webp
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
site/assets/img/free-fog-forest-480.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
site/assets/img/free-fog-forest-768.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
site/assets/img/free-fox-1200.webp
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
site/assets/img/free-fox-1600.webp
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
site/assets/img/free-fox-480.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
site/assets/img/free-fox-768.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
site/assets/img/free-golden-grass-1200.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
site/assets/img/free-golden-grass-1600.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
site/assets/img/free-golden-grass-480.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
site/assets/img/free-golden-grass-768.webp
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
site/assets/img/free-lake-person-1200.webp
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
site/assets/img/free-lake-person-1600.webp
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
site/assets/img/free-lake-person-480.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
site/assets/img/free-lake-person-768.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
site/assets/img/free-modern-portrait-1200.webp
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
site/assets/img/free-modern-portrait-1600.webp
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
site/assets/img/free-modern-portrait-480.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
site/assets/img/free-modern-portrait-768.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
site/assets/img/free-mountain-view-1200.webp
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
site/assets/img/free-mountain-view-1600.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
site/assets/img/free-mountain-view-480.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
site/assets/img/free-mountain-view-768.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
site/assets/img/free-portrait-light-1200.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
site/assets/img/free-portrait-light-1600.webp
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
site/assets/img/free-portrait-light-480.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
site/assets/img/free-portrait-light-768.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
site/assets/img/free-woman-coast-1200.webp
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
site/assets/img/free-woman-coast-1600.webp
Normal file
|
After Width: | Height: | Size: 473 KiB |
BIN
site/assets/img/free-woman-coast-480.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
site/assets/img/free-woman-coast-768.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 37 KiB |
BIN
site/assets/img/logo-160.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
site/assets/img/music-drums-1200.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
site/assets/img/music-drums-1600.webp
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
site/assets/img/music-drums-480.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
site/assets/img/music-drums-768.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
site/assets/img/music-guitar-silhouette-1200.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
site/assets/img/music-guitar-silhouette-1600.webp
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
site/assets/img/music-guitar-silhouette-480.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
site/assets/img/music-guitar-silhouette-768.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
site/assets/img/music-mic-1200.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
site/assets/img/music-mic-1600.webp
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
site/assets/img/music-mic-480.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
site/assets/img/music-mic-768.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
site/assets/img/music-strings-1200.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
site/assets/img/music-strings-1600.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
site/assets/img/music-strings-480.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
site/assets/img/music-strings-768.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
site/assets/img/music-vocal-blue-1200.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
site/assets/img/music-vocal-blue-1600.webp
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
site/assets/img/music-vocal-blue-480.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
site/assets/img/music-vocal-blue-768.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |