angeluget commited on
Commit
beaca28
·
verified ·
1 Parent(s): d49575d

Deploy color palette checker app

Browse files
Files changed (3) hide show
  1. app.js +137 -0
  2. index.html +42 -17
  3. style.css +128 -16
app.js ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const softAutumn = [
2
+ { name: "Moss", hex: "#7A8450" },
3
+ { name: "Olive", hex: "#6F6B3C" },
4
+ { name: "Sage", hex: "#8A9273" },
5
+ { name: "Camel", hex: "#B08A63" },
6
+ { name: "Terracotta", hex: "#B66545" },
7
+ { name: "Rust", hex: "#9A4F2F" },
8
+ { name: "Peach", hex: "#D7A47E" },
9
+ { name: "Dusty Coral", hex: "#C9826D" },
10
+ { name: "Warm Taupe", hex: "#8E7360" },
11
+ { name: "Mushroom", hex: "#A38B78" },
12
+ { name: "Warm Navy", hex: "#3E4D58" },
13
+ { name: "Soft Teal", hex: "#5F7E78" }
14
+ ];
15
+
16
+ const video = document.getElementById("video");
17
+ const startBtn = document.getElementById("startBtn");
18
+ const scanBtn = document.getElementById("scanBtn");
19
+ const statusEl = document.getElementById("status");
20
+ const sampleSwatch = document.getElementById("sampleSwatch");
21
+ const sampleHexEl = document.getElementById("sampleHex");
22
+ const paletteGrid = document.getElementById("paletteGrid");
23
+ const captureCanvas = document.getElementById("captureCanvas");
24
+ const ctx = captureCanvas.getContext("2d", { willReadFrequently: true });
25
+
26
+ let stream;
27
+
28
+ function hexToRgb(hex) {
29
+ const clean = hex.replace("#", "");
30
+ return {
31
+ r: parseInt(clean.slice(0, 2), 16),
32
+ g: parseInt(clean.slice(2, 4), 16),
33
+ b: parseInt(clean.slice(4, 6), 16)
34
+ };
35
+ }
36
+
37
+ function rgbToHex({ r, g, b }) {
38
+ return (
39
+ "#" +
40
+ [r, g, b]
41
+ .map((value) => value.toString(16).padStart(2, "0"))
42
+ .join("")
43
+ .toUpperCase()
44
+ );
45
+ }
46
+
47
+ function colorDistance(a, b) {
48
+ return Math.sqrt(
49
+ 2 * (a.r - b.r) ** 2 +
50
+ 4 * (a.g - b.g) ** 2 +
51
+ 3 * (a.b - b.b) ** 2
52
+ );
53
+ }
54
+
55
+ function averageCenterColor() {
56
+ const w = captureCanvas.width;
57
+ const h = captureCanvas.height;
58
+ ctx.drawImage(video, 0, 0, w, h);
59
+
60
+ const data = ctx.getImageData(0, 0, w, h).data;
61
+ let r = 0;
62
+ let g = 0;
63
+ let b = 0;
64
+ let n = 0;
65
+
66
+ for (let y = 10; y < 30; y++) {
67
+ for (let x = 10; x < 30; x++) {
68
+ const i = (y * w + x) * 4;
69
+ r += data[i];
70
+ g += data[i + 1];
71
+ b += data[i + 2];
72
+ n += 1;
73
+ }
74
+ }
75
+
76
+ return {
77
+ r: Math.round(r / n),
78
+ g: Math.round(g / n),
79
+ b: Math.round(b / n)
80
+ };
81
+ }
82
+
83
+ function renderPalette() {
84
+ paletteGrid.innerHTML = "";
85
+
86
+ for (const color of softAutumn) {
87
+ const chip = document.createElement("div");
88
+ chip.className = "chip";
89
+ chip.innerHTML = `<div class="chip-color" style="background:${color.hex}"></div>${color.name}`;
90
+ paletteGrid.appendChild(chip);
91
+ }
92
+ }
93
+
94
+ startBtn.addEventListener("click", async () => {
95
+ try {
96
+ stream = await navigator.mediaDevices.getUserMedia({
97
+ video: { facingMode: { ideal: "environment" } },
98
+ audio: false
99
+ });
100
+
101
+ video.srcObject = stream;
102
+ scanBtn.disabled = false;
103
+ statusEl.textContent = "Status: camera ready.";
104
+ } catch (error) {
105
+ statusEl.textContent = `Status: camera error: ${error.message}`;
106
+ }
107
+ });
108
+
109
+ scanBtn.addEventListener("click", () => {
110
+ if (!video.videoWidth) {
111
+ statusEl.textContent = "Status: camera is not ready yet.";
112
+ return;
113
+ }
114
+
115
+ const sample = averageCenterColor();
116
+ const sampleHex = rgbToHex(sample);
117
+ let closest;
118
+
119
+ for (const paletteColor of softAutumn) {
120
+ const distance = colorDistance(sample, hexToRgb(paletteColor.hex));
121
+ if (!closest || distance < closest.distance) {
122
+ closest = { ...paletteColor, distance };
123
+ }
124
+ }
125
+
126
+ const threshold = 85;
127
+ const isMatch = closest.distance <= threshold;
128
+
129
+ sampleSwatch.style.background = sampleHex;
130
+ sampleHexEl.textContent = sampleHex;
131
+
132
+ statusEl.innerHTML = isMatch
133
+ ? `<span class="ok">Status: Match YES.</span> Closest: ${closest.name} ${closest.hex} (distance ${closest.distance.toFixed(1)})`
134
+ : `<span class="bad">Status: Match NO.</span> Closest: ${closest.name} ${closest.hex} (distance ${closest.distance.toFixed(1)})`;
135
+ });
136
+
137
+ renderPalette();
index.html CHANGED
@@ -1,19 +1,44 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>Soft Autumn Palette Checker</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <main class="app">
11
+ <header>
12
+ <h1>Soft Autumn Palette Checker</h1>
13
+ <p>Point your camera at clothing or fabric and scan the center color.</p>
14
+ </header>
15
+
16
+ <section class="camera-wrap">
17
+ <video id="video" autoplay playsinline muted></video>
18
+ <div class="reticle" aria-hidden="true"></div>
19
+ </section>
20
+
21
+ <section class="controls">
22
+ <button id="startBtn" type="button">Start Camera</button>
23
+ <button id="scanBtn" type="button" disabled>Scan Center Color</button>
24
+ </section>
25
+
26
+ <section class="panel">
27
+ <p id="status">Status: camera not started.</p>
28
+ <div class="sample-row">
29
+ <span>Sample</span>
30
+ <div id="sampleSwatch" class="swatch"></div>
31
+ <code id="sampleHex">#------</code>
32
+ </div>
33
+ </section>
34
+
35
+ <section class="panel">
36
+ <h2>Soft Autumn Palette</h2>
37
+ <div id="paletteGrid" class="palette-grid"></div>
38
+ </section>
39
+ </main>
40
+
41
+ <canvas id="captureCanvas" width="40" height="40" hidden></canvas>
42
+ <script src="app.js"></script>
43
+ </body>
44
  </html>
style.css CHANGED
@@ -1,28 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
4
  }
5
 
6
  h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
9
  }
10
 
11
  p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
28
  }
 
1
+ :root {
2
+ --bg: #f7f3ec;
3
+ --ink: #2f2a24;
4
+ --panel: #fffaf2;
5
+ --accent: #7a5b3d;
6
+ --ok: #1f7a43;
7
+ --bad: #a1352f;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
  body {
15
+ margin: 0;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
+ color: var(--ink);
18
+ background: radial-gradient(circle at top, #fffdf8, var(--bg));
19
+ }
20
+
21
+ .app {
22
+ max-width: 760px;
23
+ margin: 0 auto;
24
+ padding: 16px;
25
  }
26
 
27
  h1 {
28
+ margin: 0 0 6px;
29
+ font-size: 1.35rem;
30
+ }
31
+
32
+ h2 {
33
+ margin: 0 0 10px;
34
+ font-size: 1rem;
35
  }
36
 
37
  p {
38
+ margin: 0 0 14px;
39
+ }
40
+
41
+ .camera-wrap {
42
+ position: relative;
43
+ border-radius: 14px;
44
+ overflow: hidden;
45
+ background: #ddd;
46
+ aspect-ratio: 3 / 4;
47
+ }
48
+
49
+ video {
50
+ width: 100%;
51
+ height: 100%;
52
+ object-fit: cover;
53
+ display: block;
54
+ }
55
+
56
+ .reticle {
57
+ position: absolute;
58
+ left: 50%;
59
+ top: 50%;
60
+ width: 70px;
61
+ height: 70px;
62
+ transform: translate(-50%, -50%);
63
+ border: 3px solid #fff;
64
+ border-radius: 10px;
65
+ box-shadow: 0 0 0 200vmax rgba(0, 0, 0, 0.1);
66
+ }
67
+
68
+ .controls {
69
+ display: flex;
70
+ gap: 10px;
71
+ margin-top: 12px;
72
+ flex-wrap: wrap;
73
+ }
74
+
75
+ button {
76
+ border: 0;
77
+ border-radius: 10px;
78
+ padding: 10px 14px;
79
+ background: var(--accent);
80
+ color: #fff;
81
+ font-weight: 600;
82
+ }
83
+
84
+ button:disabled {
85
+ opacity: 0.55;
86
+ }
87
+
88
+ .panel {
89
+ margin-top: 12px;
90
+ background: var(--panel);
91
+ border: 1px solid #eadfcd;
92
+ border-radius: 12px;
93
+ padding: 12px;
94
+ }
95
+
96
+ .sample-row {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 10px;
100
+ margin-top: 8px;
101
+ }
102
+
103
+ .swatch {
104
+ width: 34px;
105
+ height: 34px;
106
+ border-radius: 8px;
107
+ border: 1px solid #bbb;
108
+ }
109
+
110
+ .palette-grid {
111
+ display: grid;
112
+ grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
113
+ gap: 8px;
114
+ }
115
+
116
+ .chip {
117
+ border-radius: 8px;
118
+ padding: 6px;
119
+ text-align: center;
120
+ font-size: 12px;
121
+ border: 1px solid #d9cfbf;
122
+ background: #fff;
123
+ }
124
+
125
+ .chip-color {
126
+ height: 26px;
127
+ border-radius: 6px;
128
+ border: 1px solid rgba(0, 0, 0, 0.15);
129
+ margin-bottom: 4px;
130
  }
131
 
132
+ .ok {
133
+ color: var(--ok);
134
+ font-weight: 700;
 
 
 
135
  }
136
 
137
+ .bad {
138
+ color: var(--bad);
139
+ font-weight: 700;
140
  }