@tidyfactor/design 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tidyfactor +52 -0
- package/AGENTS.md +75 -0
- package/CHANGELOG.md +210 -0
- package/LICENSE +17 -0
- package/README.ar.md +398 -0
- package/README.de.md +44 -0
- package/README.es.md +44 -0
- package/README.fa.md +44 -0
- package/README.fr.md +44 -0
- package/README.md +418 -0
- package/README.pt.md +44 -0
- package/README.zh.md +44 -0
- package/SKILL-REGISTRY.md +69 -0
- package/SKILL.md +46 -0
- package/VISION.md +43 -0
- package/assets/blog.png +0 -0
- package/assets/content_layout.png +0 -0
- package/assets/content_output.png +0 -0
- package/assets/dark.png +0 -0
- package/assets/dashboard_layout.png +0 -0
- package/assets/dashboard_output.png +0 -0
- package/assets/ecommerce_layout.png +0 -0
- package/assets/ecommerce_output.png +0 -0
- package/assets/github-social-preview-2.png +0 -0
- package/assets/github-social-preview.png +0 -0
- package/assets/hero-banner.png +0 -0
- package/assets/light.png +0 -0
- package/assets/media_layout.png +0 -0
- package/assets/media_output.png +0 -0
- package/assets/og-default.png +0 -0
- package/assets/video.png +0 -0
- package/bin/add-skill.js +31 -0
- package/bin/create-kit.js +372 -0
- package/bin/remove-skill.js +137 -0
- package/brand.json +279 -0
- package/memory/01-design-schools.md +85 -0
- package/memory/02-design-tokens.md +41 -0
- package/memory/03-narrative-conversion.md +47 -0
- package/memory/04-motion-principles.md +44 -0
- package/memory/05-component-anatomy.md +34 -0
- package/memory/06-quality-bar.md +83 -0
- package/memory/07-consistency-contract.md +33 -0
- package/memory/08-arabic-bilingual.md +54 -0
- package/memory/09-prototype-flow.md +36 -0
- package/memory/10-python-tooling.md +64 -0
- package/memory/11-brand-json-v2.md +86 -0
- package/memory/12-typography-matrix.md +31 -0
- package/memory/13-layout-archetypes.md +30 -0
- package/memory/14-nav-footer-catalog.md +26 -0
- package/memory/15-performance-budget.md +18 -0
- package/memory/16-design-movements-guide.md +1084 -0
- package/memory/17-storytelling-industries-ux.md +143 -0
- package/memory/18-design-decision-engine.md +81 -0
- package/package.json +64 -0
- package/references/commands/_template.md +27 -0
- package/references/commands/assets.md +35 -0
- package/references/commands/audit.md +15 -0
- package/references/commands/brand.md +85 -0
- package/references/commands/brief.md +107 -0
- package/references/commands/clone.md +26 -0
- package/references/commands/components.md +46 -0
- package/references/commands/dashboard.md +45 -0
- package/references/commands/deploy.md +45 -0
- package/references/commands/flow.md +44 -0
- package/references/commands/handoff.md +53 -0
- package/references/commands/i18n.md +64 -0
- package/references/commands/init.md +30 -0
- package/references/commands/layout.md +81 -0
- package/references/commands/motion.md +62 -0
- package/references/commands/nav-footer.md +54 -0
- package/references/commands/page.md +45 -0
- package/references/commands/palette.md +35 -0
- package/references/commands/perf.md +57 -0
- package/references/commands/retrofit.md +15 -0
- package/references/commands/school.md +41 -0
- package/references/commands/states.md +48 -0
- package/references/commands/study.md +98 -0
- package/references/commands/tokens.md +45 -0
- package/references/commands/typography.md +87 -0
- package/references/foundations/daisyui.md +44 -0
- package/references/foundations/hybrid.md +32 -0
- package/references/foundations/native.md +31 -0
- package/references/foundations/tailwind-utility.md +43 -0
- package/references/memory/01-design-schools.md +85 -0
- package/references/memory/02-design-tokens.md +41 -0
- package/references/memory/03-narrative-conversion.md +47 -0
- package/references/memory/04-motion-principles.md +44 -0
- package/references/memory/05-component-anatomy.md +34 -0
- package/references/memory/06-quality-bar.md +83 -0
- package/references/memory/07-consistency-contract.md +33 -0
- package/references/memory/08-arabic-bilingual.md +54 -0
- package/references/memory/09-prototype-flow.md +36 -0
- package/references/memory/10-python-tooling.md +64 -0
- package/references/memory/11-brand-json-v2.md +86 -0
- package/references/memory/12-typography-matrix.md +31 -0
- package/references/memory/13-layout-archetypes.md +30 -0
- package/references/memory/14-nav-footer-catalog.md +26 -0
- package/references/memory/15-performance-budget.md +18 -0
- package/references/memory/16-design-movements-guide.md +1084 -0
- package/references/memory/17-storytelling-industries-ux.md +143 -0
- package/references/memory/18-design-decision-engine.md +81 -0
- package/references/memory/architecture.md +39 -0
- package/references/memory/decision-points.md +50 -0
- package/references/memory/foundations.md +15 -0
- package/references/memory/quality-bar.md +20 -0
- package/references/tidyfactor-vision.md +66 -0
- package/references/workflow.md +49 -0
- package/references/workflows/audit-prototype.md +26 -0
- package/references/workflows/brief.md +30 -0
- package/references/workflows/clone-prototype.md +27 -0
- package/references/workflows/init-prototype.md +28 -0
- package/references/workflows/retrofit-prototype.md +25 -0
- package/scripts/__pycache__/_utils.cpython-312.pyc +0 -0
- package/scripts/_utils.py +60 -0
- package/scripts/build.py +194 -0
- package/scripts/check_alpha.py +52 -0
- package/scripts/extract_palette.py +139 -0
- package/scripts/generate_transitions.py +105 -0
- package/scripts/inspect_images.py +48 -0
- package/scripts/minify_assets.py +140 -0
- package/scripts/optimize_assets.py +91 -0
- package/scripts/optimize_images.py +176 -0
- package/scripts/prepare_images.py +67 -0
- package/scripts/remove_backgrounds.py +61 -0
- package/scripts/test_build.py +228 -0
- package/templates/design-system/base.css +75 -0
- package/templates/design-system/components.css +229 -0
- package/templates/design-system/interactions.js +31 -0
- package/templates/design-system/motion.js +42 -0
- package/templates/design-system/tokens.css +81 -0
- package/templates/design-system/utilities.css +47 -0
- package/templates/index.html +84 -0
- package/templates/proto-nav.js +56 -0
- package/tools/build-skill.js +126 -0
- package/tools/validate-skill.js +144 -0
- package/tools/validate_skill.py +121 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Shared utilities for the Cinematic Landing Kit asset pipeline."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from PIL import Image
|
|
7
|
+
except ImportError:
|
|
8
|
+
Image = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def crop_cover_16_9(img, target_w: int = 1280, target_h: int = 720):
|
|
12
|
+
"""Crop-resize an image to a target 16:9 frame (object-cover style)."""
|
|
13
|
+
if Image is None:
|
|
14
|
+
raise ImportError("Pillow is required for crop_cover_16_9. Run: pip install Pillow")
|
|
15
|
+
img = img.convert("RGB")
|
|
16
|
+
img_ratio = img.width / img.height
|
|
17
|
+
target_ratio = target_w / target_h
|
|
18
|
+
if img_ratio > target_ratio:
|
|
19
|
+
new_width = int(target_ratio * img.height)
|
|
20
|
+
offset = (img.width - new_width) // 2
|
|
21
|
+
img = img.crop((offset, 0, offset + new_width, img.height))
|
|
22
|
+
else:
|
|
23
|
+
new_height = int(img.width / target_ratio)
|
|
24
|
+
offset = (img.height - new_height) // 2
|
|
25
|
+
img = img.crop((0, offset, img.width, offset + new_height))
|
|
26
|
+
return img.resize((target_w, target_h), Image.Resampling.LANCZOS)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_NATURAL_SORT_RE = re.compile(r"(\d+)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def natural_sort_key(s: str):
|
|
33
|
+
"""Sort key for filenames with embedded numbers.
|
|
34
|
+
|
|
35
|
+
'k2.jpg' sorts before 'k10.jpg', matching human expectations.
|
|
36
|
+
"""
|
|
37
|
+
parts = _NATURAL_SORT_RE.split(s)
|
|
38
|
+
return [int(p) if p.isdigit() else p.lower() for p in parts]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def split_spec(spec: str) -> list[str]:
|
|
42
|
+
"""Split 'src:dest[:opts]' with Windows drive-letter awareness.
|
|
43
|
+
|
|
44
|
+
A single-alpha character immediately before ':' is treated as a drive-letter
|
|
45
|
+
prefix (e.g. C:\\path) rather than a field separator. All other colons are
|
|
46
|
+
normal field delimiters.
|
|
47
|
+
"""
|
|
48
|
+
parts: list[str] = []
|
|
49
|
+
current = ""
|
|
50
|
+
for ch in spec:
|
|
51
|
+
if ch == ":":
|
|
52
|
+
if len(current) == 1 and current[0].isalpha():
|
|
53
|
+
current += ch
|
|
54
|
+
else:
|
|
55
|
+
parts.append(current)
|
|
56
|
+
current = ""
|
|
57
|
+
else:
|
|
58
|
+
current += ch
|
|
59
|
+
parts.append(current)
|
|
60
|
+
return parts
|
package/scripts/build.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Main Production Build Orchestrator
|
|
4
|
+
Runs all production build steps in sequence:
|
|
5
|
+
1. Bundles and minifies JS and CSS assets.
|
|
6
|
+
2. Scans and optimizes all images in assets/image/.
|
|
7
|
+
3. Executes the build test suite. Aborts the build on failure.
|
|
8
|
+
4. Performs cache-busting on index.html, updating asset query strings (?v=) with a timestamp.
|
|
9
|
+
5. Packages the clean production-ready files into release/deploy.zip.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python scripts/build.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import zipfile
|
|
20
|
+
import subprocess
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# Paths relative to this script (inside scripts/)
|
|
24
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
25
|
+
PROJECT_ROOT = SCRIPTS_DIR.parent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_command(command_args, description):
|
|
29
|
+
print(f"\n>>> Running: {description}...")
|
|
30
|
+
try:
|
|
31
|
+
# Run sub-script and redirect output to stdout
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
[sys.executable] + command_args,
|
|
34
|
+
stdout=subprocess.PIPE,
|
|
35
|
+
stderr=subprocess.PIPE,
|
|
36
|
+
text=True,
|
|
37
|
+
check=True
|
|
38
|
+
)
|
|
39
|
+
print(result.stdout)
|
|
40
|
+
return True
|
|
41
|
+
except subprocess.CalledProcessError as e:
|
|
42
|
+
print(f" [ERROR] {description} failed.")
|
|
43
|
+
print(e.stdout)
|
|
44
|
+
print(e.stderr)
|
|
45
|
+
return False
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(f" [ERROR] Failed to run command: {e}")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def update_cache_busting_versions():
|
|
52
|
+
print("\n>>> Running: index.html Cache-Busting Version Updates...")
|
|
53
|
+
index_path = PROJECT_ROOT / "index.html"
|
|
54
|
+
if not index_path.exists():
|
|
55
|
+
print(" [ERROR] index.html not found, skipping version updates.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Use current datetime stamp as unique version query string
|
|
59
|
+
version_stamp = time.strftime("%Y%m%d%H%M%S")
|
|
60
|
+
print(f" Target version code: {version_stamp}")
|
|
61
|
+
|
|
62
|
+
with open(index_path, "r", encoding="utf-8") as f:
|
|
63
|
+
content = f.read()
|
|
64
|
+
|
|
65
|
+
# Regex matches to replace query strings in href and src attributes
|
|
66
|
+
new_content = re.sub(
|
|
67
|
+
r'(href=["\']assets/css/style\.min\.css)\?v=[^"\'\s]*([\x22\x27])',
|
|
68
|
+
r'\1?v=' + version_stamp + r'\2',
|
|
69
|
+
content
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
new_content = re.sub(
|
|
73
|
+
r'(src=["\']assets/js/bundle\.min\.js)\?v=[^"\'\s]*([\x22\x27])',
|
|
74
|
+
r'\1?v=' + version_stamp + r'\2',
|
|
75
|
+
new_content
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if content != new_content:
|
|
79
|
+
with open(index_path, "w", encoding="utf-8") as f:
|
|
80
|
+
f.write(new_content)
|
|
81
|
+
print(f" [SUCCESS] index.html asset references updated with version code '?v={version_stamp}'.")
|
|
82
|
+
else:
|
|
83
|
+
print(" [INFO] No version references updated in index.html.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def package_release():
|
|
87
|
+
print("\n>>> Running: Packaging Clean Production Release...")
|
|
88
|
+
release_dir = PROJECT_ROOT / "release"
|
|
89
|
+
release_dir.mkdir(exist_ok=True)
|
|
90
|
+
|
|
91
|
+
zip_path = release_dir / "deploy.zip"
|
|
92
|
+
if zip_path.exists():
|
|
93
|
+
try:
|
|
94
|
+
zip_path.unlink()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f" [ERROR] Could not delete old release package: {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
print(f" Creating ZIP archive: {zip_path.relative_to(PROJECT_ROOT)}")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
103
|
+
# 1. Add index.html
|
|
104
|
+
index_path = PROJECT_ROOT / "index.html"
|
|
105
|
+
if index_path.exists():
|
|
106
|
+
zipf.write(index_path, "index.html")
|
|
107
|
+
print(" + index.html")
|
|
108
|
+
|
|
109
|
+
# 2. Recurse assets/
|
|
110
|
+
assets_dir = PROJECT_ROOT / "assets"
|
|
111
|
+
added_count = 0
|
|
112
|
+
for root, _, files in os.walk(assets_dir):
|
|
113
|
+
for file in files:
|
|
114
|
+
file_path = Path(root) / file
|
|
115
|
+
|
|
116
|
+
# Compute relative path string
|
|
117
|
+
rel_path = file_path.relative_to(PROJECT_ROOT)
|
|
118
|
+
rel_path_str = rel_path.as_posix()
|
|
119
|
+
|
|
120
|
+
# Skip backups and system dotfiles
|
|
121
|
+
if "_backup" in file_path.name or file_path.name.startswith("."):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Clean production exclusions:
|
|
125
|
+
# - Under assets/css/: skip style.css (keep style.min.css)
|
|
126
|
+
if rel_path_str.startswith("assets/css/"):
|
|
127
|
+
if file_path.name == "style.css":
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# - Under assets/js/: skip main.js and unneeded library folders (keep only bundle.min.js)
|
|
131
|
+
if rel_path_str.startswith("assets/js/"):
|
|
132
|
+
if file_path.name != "bundle.min.js":
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
zipf.write(file_path, rel_path)
|
|
136
|
+
added_count += 1
|
|
137
|
+
print(f" + assets/ ({added_count} files included, unminified files and libraries excluded)")
|
|
138
|
+
|
|
139
|
+
print(f" [SUCCESS] Created release package successfully: release/deploy.zip")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f" [ERROR] Failed to compile ZIP archive: {e}")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
start_time = time.time()
|
|
147
|
+
|
|
148
|
+
print("="*60)
|
|
149
|
+
print(" NOOR AL-ISLAM LANDING PAGE — PRODUCTION BUILDER")
|
|
150
|
+
print("="*60)
|
|
151
|
+
|
|
152
|
+
# 1. Run Minifier & Bundler
|
|
153
|
+
assets_success = run_command(
|
|
154
|
+
[str(SCRIPTS_DIR / "minify_assets.py")],
|
|
155
|
+
"Minifying and Bundling CSS & JS Assets"
|
|
156
|
+
)
|
|
157
|
+
if not assets_success:
|
|
158
|
+
print("Build aborted due to JS/CSS bundling errors.")
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
# 2. Run Image Optimizer
|
|
162
|
+
images_success = run_command(
|
|
163
|
+
[str(SCRIPTS_DIR / "optimize_images.py"), "assets/image"],
|
|
164
|
+
"Optimizing Project Images"
|
|
165
|
+
)
|
|
166
|
+
if not images_success:
|
|
167
|
+
print(" [WARNING] Image optimization script reported warnings/errors. Continuing build...")
|
|
168
|
+
|
|
169
|
+
# 3. Run Build Integrity Tests
|
|
170
|
+
tests_success = run_command(
|
|
171
|
+
[str(SCRIPTS_DIR / "test_build.py")],
|
|
172
|
+
"Executing Automated Test Suite"
|
|
173
|
+
)
|
|
174
|
+
if not tests_success:
|
|
175
|
+
print("\n[FAIL] BUILD ABORTED: The automated test suite reported failures.")
|
|
176
|
+
print("Please resolve the failures listed above before deploying.")
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
|
|
179
|
+
# 4. Cache Busting Version Updater
|
|
180
|
+
update_cache_busting_versions()
|
|
181
|
+
|
|
182
|
+
# 5. Package ZIP Release
|
|
183
|
+
package_release()
|
|
184
|
+
|
|
185
|
+
end_time = time.time()
|
|
186
|
+
elapsed = end_time - start_time
|
|
187
|
+
|
|
188
|
+
print("\n" + "="*60)
|
|
189
|
+
print(f"[SUCCESS] PRODUCTION BUILD & PACKAGING COMPLETED IN {elapsed:.2f}s!")
|
|
190
|
+
print("="*60)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from PIL import Image
|
|
7
|
+
except ImportError:
|
|
8
|
+
print("Pillow is required. Run: pip install Pillow", file=sys.stderr)
|
|
9
|
+
sys.exit(1)
|
|
10
|
+
|
|
11
|
+
def check_alpha(path):
|
|
12
|
+
if not os.path.exists(path):
|
|
13
|
+
print(f"- {path}: file not found")
|
|
14
|
+
return
|
|
15
|
+
try:
|
|
16
|
+
with Image.open(path) as img:
|
|
17
|
+
if img.mode == 'RGBA':
|
|
18
|
+
alpha = img.split()[3]
|
|
19
|
+
extrema = alpha.getextrema()
|
|
20
|
+
has_transparency = extrema[0] < 255
|
|
21
|
+
print(f"- {path}: RGBA alpha_range={extrema} transparent={has_transparency}")
|
|
22
|
+
else:
|
|
23
|
+
print(f"- {path}: {img.mode} no alpha channel")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"- {path}: ERROR {e}")
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
description="Check whether images have an alpha (transparency) channel."
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"paths",
|
|
33
|
+
nargs="*",
|
|
34
|
+
help="One or more image file paths. If a path is a directory, all PNG/WEBP files inside it are checked.",
|
|
35
|
+
)
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
if not args.paths:
|
|
39
|
+
parser.print_help()
|
|
40
|
+
sys.exit(0)
|
|
41
|
+
|
|
42
|
+
print("Alpha Channel Check:")
|
|
43
|
+
for p in args.paths:
|
|
44
|
+
if os.path.isdir(p):
|
|
45
|
+
for f in sorted(os.listdir(p)):
|
|
46
|
+
if f.lower().endswith((".png", ".webp", ".tga")):
|
|
47
|
+
check_alpha(os.path.join(p, f))
|
|
48
|
+
else:
|
|
49
|
+
check_alpha(p)
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
extract_palette.py — Design System Palette & Color Extraction Tool
|
|
4
|
+
Extracts dominant brand colors, background tones, text colors, and WCAG contrast scores
|
|
5
|
+
from reference screenshots, logos, or hero images to generate brand.json and tokens.css.
|
|
6
|
+
|
|
7
|
+
Dependencies:
|
|
8
|
+
pip install pillow
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python scripts/extract_palette.py assets/logo.png --json brand.json
|
|
12
|
+
python scripts/extract_palette.py reference_shot.png --css design-system/tokens.css
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import argparse
|
|
19
|
+
from PIL import Image
|
|
20
|
+
|
|
21
|
+
def get_rgb_luminance(rgb):
|
|
22
|
+
"""Calculates relative luminance according to WCAG 2.1 specifications."""
|
|
23
|
+
r, g, b = [x / 255.0 for x in rgb]
|
|
24
|
+
r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
|
|
25
|
+
g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
|
|
26
|
+
b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
|
|
27
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
28
|
+
|
|
29
|
+
def contrast_ratio(rgb1, rgb2):
|
|
30
|
+
"""Calculates WCAG contrast ratio between two RGB colors (1 to 21)."""
|
|
31
|
+
l1 = get_rgb_luminance(rgb1)
|
|
32
|
+
l2 = get_rgb_luminance(rgb2)
|
|
33
|
+
lighter = max(l1, l2)
|
|
34
|
+
darker = min(l1, l2)
|
|
35
|
+
return (lighter + 0.05) / (darker + 0.05)
|
|
36
|
+
|
|
37
|
+
def rgb_to_hex(rgb):
|
|
38
|
+
return f"#{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
|
|
39
|
+
|
|
40
|
+
def extract_dominant_colors(image_path, num_colors=5):
|
|
41
|
+
if not os.path.exists(image_path):
|
|
42
|
+
print(f"ERROR: File not found: {image_path}", file=sys.stderr)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
img = Image.open(image_path).convert("RGB")
|
|
46
|
+
img.thumbnail((200, 200), Image.Resampling.LANCZOS)
|
|
47
|
+
|
|
48
|
+
# Quantize image to extract primary color palette
|
|
49
|
+
quantized = img.quantize(colors=num_colors, method=Image.Quantize.MEDIANCUT)
|
|
50
|
+
palette = quantized.getpalette()[:num_colors * 3]
|
|
51
|
+
|
|
52
|
+
colors = []
|
|
53
|
+
for i in range(0, len(palette), 3):
|
|
54
|
+
rgb = tuple(palette[i:i+3])
|
|
55
|
+
colors.append(rgb)
|
|
56
|
+
|
|
57
|
+
return colors
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
parser = argparse.ArgumentParser(description="Extract brand color palettes and WCAG contrast metrics.")
|
|
61
|
+
parser.add_argument("input", help="Path to input image (logo, screenshot, brand photo)")
|
|
62
|
+
parser.add_argument("--json", "-j", help="Output path for brand.json update")
|
|
63
|
+
parser.add_argument("--css", "-c", help="Output path for tokens.css variables")
|
|
64
|
+
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
|
|
67
|
+
colors = extract_dominant_colors(args.input)
|
|
68
|
+
if not colors:
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
hex_colors = [rgb_to_hex(c) for c in colors]
|
|
72
|
+
|
|
73
|
+
primary = hex_colors[0]
|
|
74
|
+
secondary = hex_colors[1] if len(hex_colors) > 1 else "#06B6D4"
|
|
75
|
+
accent = hex_colors[2] if len(hex_colors) > 2 else "#F59E0B"
|
|
76
|
+
dark_bg = "#0F172A"
|
|
77
|
+
light_bg = "#F8FAFC"
|
|
78
|
+
|
|
79
|
+
cr_on_dark = contrast_ratio(colors[0], (15, 23, 42))
|
|
80
|
+
cr_on_light = contrast_ratio(colors[0], (248, 250, 252))
|
|
81
|
+
|
|
82
|
+
if sys.platform == "win32":
|
|
83
|
+
try:
|
|
84
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
print(f"[OK] Extracted Palette from {args.input}:")
|
|
88
|
+
print(f" Primary: {primary} (WCAG Contrast vs Dark: {cr_on_dark:.2f}:1, Light: {cr_on_light:.2f}:1)")
|
|
89
|
+
print(f" Secondary: {secondary}")
|
|
90
|
+
print(f" Accent: {accent}")
|
|
91
|
+
|
|
92
|
+
if args.json:
|
|
93
|
+
brand_data = {}
|
|
94
|
+
if os.path.exists(args.json):
|
|
95
|
+
try:
|
|
96
|
+
with open(args.json, "r", encoding="utf-8") as f:
|
|
97
|
+
brand_data = json.load(f)
|
|
98
|
+
except Exception:
|
|
99
|
+
brand_data = {}
|
|
100
|
+
|
|
101
|
+
if "colors" not in brand_data or not isinstance(brand_data["colors"], dict):
|
|
102
|
+
brand_data["colors"] = {}
|
|
103
|
+
|
|
104
|
+
# Handle brand.json v2 dual-mode schema if present
|
|
105
|
+
if "light" in brand_data["colors"] and isinstance(brand_data["colors"]["light"], dict):
|
|
106
|
+
brand_data["colors"]["light"]["primary"] = primary
|
|
107
|
+
brand_data["colors"]["light"]["secondary"] = secondary
|
|
108
|
+
brand_data["colors"]["light"]["accent"] = accent
|
|
109
|
+
if "dark" in brand_data["colors"] and isinstance(brand_data["colors"]["dark"], dict):
|
|
110
|
+
brand_data["colors"]["dark"]["secondary"] = secondary
|
|
111
|
+
brand_data["colors"]["dark"]["accent"] = accent
|
|
112
|
+
else:
|
|
113
|
+
# Fallback for flat structure
|
|
114
|
+
brand_data["colors"]["primary"] = primary
|
|
115
|
+
brand_data["colors"]["secondary"] = secondary
|
|
116
|
+
brand_data["colors"]["accent"] = accent
|
|
117
|
+
brand_data["colors"]["neutralDark"] = dark_bg
|
|
118
|
+
brand_data["colors"]["neutralLight"] = light_bg
|
|
119
|
+
|
|
120
|
+
with open(args.json, "w", encoding="utf-8") as f:
|
|
121
|
+
json.dump(brand_data, f, indent=2)
|
|
122
|
+
print(f"✓ Palette updated in: {args.json}")
|
|
123
|
+
|
|
124
|
+
if args.css:
|
|
125
|
+
css_vars = f"""/* Auto-generated Design Tokens from {args.input} */
|
|
126
|
+
:root {{
|
|
127
|
+
--color-primary: {primary};
|
|
128
|
+
--color-secondary: {secondary};
|
|
129
|
+
--color-accent: {accent};
|
|
130
|
+
--color-dark: {dark_bg};
|
|
131
|
+
--color-light: {light_bg};
|
|
132
|
+
}}
|
|
133
|
+
"""
|
|
134
|
+
with open(args.css, "a", encoding="utf-8") as f:
|
|
135
|
+
f.write(css_vars)
|
|
136
|
+
print(f"✓ CSS Variables appended to: {args.css}")
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from PIL import Image
|
|
7
|
+
except ImportError:
|
|
8
|
+
print("Required package not installed. Run: pip install Pillow", file=sys.stderr)
|
|
9
|
+
sys.exit(1)
|
|
10
|
+
|
|
11
|
+
from _utils import crop_cover_16_9, natural_sort_key
|
|
12
|
+
|
|
13
|
+
TARGET_W, TARGET_H = 1280, 720
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description=(
|
|
19
|
+
"Generate a JPEG frame-sequence from N keyframe images using eased cross-blends. "
|
|
20
|
+
"Output frames are named f000.jpg, f001.jpg, … and are suitable for scrubbing on a <canvas>."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--keyframes-dir",
|
|
25
|
+
"-k",
|
|
26
|
+
required=True,
|
|
27
|
+
help="Directory containing numbered keyframe images (k1.png k2.png … or any sorted order).",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--output-dir",
|
|
31
|
+
"-o",
|
|
32
|
+
default="assets/seq",
|
|
33
|
+
help="Directory to write the output frame sequence (default: assets/seq).",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--src-dir",
|
|
37
|
+
help="Optional directory to save the normalised 1280x720 keyframes (default: <output-dir>/../seq-src).",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--total-frames",
|
|
41
|
+
"-f",
|
|
42
|
+
type=int,
|
|
43
|
+
default=93,
|
|
44
|
+
help="Total number of output frames (default: 93).",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--quality",
|
|
48
|
+
"-q",
|
|
49
|
+
type=int,
|
|
50
|
+
default=80,
|
|
51
|
+
help="JPEG quality for output frames (default: 80).",
|
|
52
|
+
)
|
|
53
|
+
args = parser.parse_args()
|
|
54
|
+
|
|
55
|
+
kf_dir = args.keyframes_dir
|
|
56
|
+
if not os.path.isdir(kf_dir):
|
|
57
|
+
print(f"Error: keyframes directory '{kf_dir}' does not exist.", file=sys.stderr)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
kf_files = sorted(
|
|
61
|
+
(f for f in os.listdir(kf_dir)
|
|
62
|
+
if os.path.splitext(f)[1].lower() in {".png", ".jpg", ".jpeg", ".webp"}),
|
|
63
|
+
key=natural_sort_key,
|
|
64
|
+
)
|
|
65
|
+
if len(kf_files) < 2:
|
|
66
|
+
print(f"Error: at least 2 keyframe images are required; found {len(kf_files)} in '{kf_dir}'.", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
os.makedirs(args.output_dir, exist_ok=True)
|
|
70
|
+
src_dir = args.src_dir or os.path.join(os.path.dirname(args.output_dir), "seq-src")
|
|
71
|
+
os.makedirs(src_dir, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
keyframes = []
|
|
74
|
+
for i, fname in enumerate(kf_files):
|
|
75
|
+
path = os.path.join(kf_dir, fname)
|
|
76
|
+
img = crop_cover_16_9(Image.open(path))
|
|
77
|
+
keyframes.append(img)
|
|
78
|
+
dest = os.path.join(src_dir, f"k{i + 1}.jpg")
|
|
79
|
+
img.save(dest, "JPEG", quality=90)
|
|
80
|
+
print(f"[keyframe] {path} -> {dest}")
|
|
81
|
+
|
|
82
|
+
n_segments = len(keyframes) - 1
|
|
83
|
+
total_frames = args.total_frames
|
|
84
|
+
seg_frames = total_frames // n_segments
|
|
85
|
+
remainder = total_frames - seg_frames * n_segments
|
|
86
|
+
|
|
87
|
+
frame_idx = 0
|
|
88
|
+
for seg in range(n_segments):
|
|
89
|
+
start_img = keyframes[seg]
|
|
90
|
+
end_img = keyframes[seg + 1]
|
|
91
|
+
count = seg_frames + (1 if seg < remainder else 0)
|
|
92
|
+
for j in range(count):
|
|
93
|
+
# When count == 1 the single frame is an unblended copy of start_img
|
|
94
|
+
# (t = 0 / 1 = 0, blend weight 0 → pure start_img).
|
|
95
|
+
t = j / max(count - 1, 1)
|
|
96
|
+
ease_t = t * t * (3.0 - 2.0 * t)
|
|
97
|
+
blended = Image.blend(start_img, end_img, ease_t)
|
|
98
|
+
dest = os.path.join(args.output_dir, f"f{frame_idx:03d}.jpg")
|
|
99
|
+
blended.save(dest, "JPEG", quality=args.quality)
|
|
100
|
+
frame_idx += 1
|
|
101
|
+
|
|
102
|
+
print(f"[done] Generated {frame_idx} frames in {args.output_dir}/")
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from PIL import Image
|
|
7
|
+
except ImportError:
|
|
8
|
+
print("Pillow is required. Run: pip install Pillow", file=sys.stderr)
|
|
9
|
+
sys.exit(1)
|
|
10
|
+
|
|
11
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".tiff", ".bmp", ".tga"}
|
|
12
|
+
|
|
13
|
+
def inspect(path):
|
|
14
|
+
try:
|
|
15
|
+
with Image.open(path) as img:
|
|
16
|
+
print(f"- {path}: size={img.size} format={img.format} mode={img.mode}")
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print(f"- {path}: ERROR {e}")
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
description="Print size, format, and colour-mode of image files."
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"paths",
|
|
26
|
+
nargs="*",
|
|
27
|
+
help=(
|
|
28
|
+
"One or more image file paths or directories. "
|
|
29
|
+
"Directories are scanned for common image extensions."
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
if not args.paths:
|
|
35
|
+
parser.print_help()
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
|
|
38
|
+
print("Image Inspection:")
|
|
39
|
+
for p in args.paths:
|
|
40
|
+
if os.path.isdir(p):
|
|
41
|
+
for f in sorted(os.listdir(p)):
|
|
42
|
+
if os.path.splitext(f)[1].lower() in IMAGE_EXTENSIONS:
|
|
43
|
+
inspect(os.path.join(p, f))
|
|
44
|
+
else:
|
|
45
|
+
inspect(p)
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|