05サーモスタットの風景窓
丸いスマートサーモスタット — 上部の窓には山から昇る太陽と映る湖があり、アイコンを押すと太陽の高さと温度の数字が一緒に変わる。
使いどころスマートホーム製品の詳細画面
clip-pathrotatetransition

<main class="stage">
<div class="th is-demo" id="thRoot" data-mode="sun">
<div class="th__device">
<div class="th__window">
<span class="th__sky"></span>
<span class="th__sunwrap"><span class="th__sun"></span></span>
<span class="th__mount th__mount--l"></span>
<span class="th__mount th__mount--r"></span>
<span class="th__lake"><span class="th__refl"></span></span>
</div>
<div class="th__panel">
<div class="th__rowtop">
<span class="th__dial"><span class="th__dialmark"></span></span>
<span class="th__temp" id="thTemp">27°</span>
</div>
<div class="th__modes">
<button class="th__mode" type="button" data-mode="snow" aria-pressed="false" aria-label="雪モード">
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M220 128a92 92 0 1 1-92-92a92.1 92.1 0 0 1 92 92" opacity=".2"/><path d="M223.77 150.09a8 8 0 0 1-5.86 9.68l-24.64 6l6.46 24.11a8 8 0 0 1-5.66 9.8a8.3 8.3 0 0 1-2.07.32a8 8 0 0 1-7.72-5.93l-7.72-28.8L136 141.86v46.83l21.66 21.65a8 8 0 0 1-11.32 11.32L128 203.31l-18.34 18.35a8 8 0 0 1-11.32-11.32L120 188.69v-46.83l-40.55 23.41l-7.72 28.8A8 8 0 0 1 64 200a8.3 8.3 0 0 1-2.08-.27a8 8 0 0 1-5.66-9.8l6.46-24.11l-24.64-6a8 8 0 0 1 3.82-15.54l29.45 7.23L112 128l-40.64-23.46l-29.45 7.23A8 8 0 0 1 40 112a8 8 0 0 1-1.91-15.77l24.64-6l-6.46-24.16a8 8 0 0 1 15.46-4.14l7.72 28.8L120 114.14V67.31L98.34 45.66a8 8 0 0 1 11.32-11.32L128 52.69l18.34-18.35a8 8 0 0 1 11.32 11.32L136 67.31v46.83l40.55-23.41l7.72-28.8a8 8 0 0 1 15.46 4.14l-6.46 24.11l24.64 6A8 8 0 0 1 216 112a8 8 0 0 1-1.91-.23l-29.45-7.23L144 128l40.64 23.46l29.45-7.23a8 8 0 0 1 9.68 5.86"/></g></svg>
</button>
<button class="th__mode" type="button" data-mode="sun" aria-pressed="true" aria-label="晴れモード">
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M184 128a56 56 0 1 1-56-56a56 56 0 0 1 56 56" opacity=".2"/><path d="M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m72 88a64 64 0 1 1-64-64a64.07 64.07 0 0 1 64 64m-16 0a48 48 0 1 0-48 48a48.05 48.05 0 0 0 48-48M58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16"/></g></svg>
</button>
<button class="th__mode" type="button" data-mode="cloud" aria-pressed="false" aria-label="曇りモード">
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M240 128a80 80 0 0 1-80 80H72A56 56 0 1 1 85.92 97.74v.1A80 80 0 0 1 240 128" opacity=".2"/><path d="M160 40a88.09 88.09 0 0 0-78.71 48.67A64 64 0 1 0 72 216h88a88 88 0 0 0 0-176m0 160H72a48 48 0 0 1 0-96c1.1 0 2.2 0 3.29.11A88 88 0 0 0 72 128a8 8 0 0 0 16 0a72 72 0 1 1 72 72"/></g></svg>
</button>
<button class="th__mode" type="button" data-mode="flame" aria-pressed="false" aria-label="暖房モード">
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M208 144a80 80 0 0 1-77.94 80A40 40 0 0 0 168 184c0-40-40-64-40-64s-40 24-40 64a40 40 0 0 0 37.94 40A80 80 0 0 1 48 144c0-72 80-120 80-120s80 48 80 120" opacity=".2"/><path d="M173.79 51.48a221.3 221.3 0 0 0-41.67-34.34a8 8 0 0 0-8.24 0a221.3 221.3 0 0 0-41.67 34.34C54.59 80.48 40 112.47 40 144a88 88 0 0 0 176 0c0-31.53-14.59-63.52-42.21-92.52M96 184c0-27.67 22.53-47.28 32-54.3c9.48 7 32 26.63 32 54.3a32 32 0 0 1-64 0m77.27 15.93A47.8 47.8 0 0 0 176 184c0-44-42.09-69.79-43.88-70.86a8 8 0 0 0-8.24 0C122.09 114.21 80 140 80 184a47.8 47.8 0 0 0 2.73 15.93A71.88 71.88 0 0 1 56 144c0-34.41 20.4-63.15 37.52-81.19A216.2 216.2 0 0 1 128 33.54a215.8 215.8 0 0 1 34.48 29.27C193.49 95.5 200 125 200 144a71.88 71.88 0 0 1-26.73 55.93"/></g></svg>
</button>
</div>
</div>
</div>
<p class="th__hint">雪・晴れ・曇り・暖房アイコンを押してみて</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 온도조절기 풍경 창 — 둥근 기기 안에 산·호수·해가 뜨는 창을 넣는다.
// 해의 높이는 data-mode(snow/sun/cloud/flame)가 정하고 transition 으로 움직이며,
// 다이얼 눈금은 모드마다 rotate 로 돌아간다. 호수 반사는 느리게 일렁인다.
$duration: $dur-base;
$easing: $ease-spring;
$color: $tick-up;
$case: #23262c;
$case-edge: #17191e;
$panel: $subject-cream;
$sky-a: #c97e5a;
$sky-b: #ebe1d6;
$sunpale: #f5e6a8;
$peak: #5b6067;
$peak-deep: #43474d;
$water: #6d94a8;
@include stage($stage-ink);
.th {
display: flex; flex-direction: column; align-items: center; gap: $sp-2;
width: min(100%, 320px);
}
.th__device {
position: relative; width: 152px; height: 152px;
border-radius: 50%; overflow: hidden;
background: $case;
box-shadow: 0 0 0 6px $case-edge;
}
// ── 상단 풍경 창
.th__window {
position: absolute; left: 16px; right: 16px; top: 12px; height: 62px;
border-radius: $r-card; overflow: hidden;
background: linear-gradient(180deg, $sky-a 0%, $sky-b 100%);
}
.th__sunwrap {
position: absolute; left: 50%; top: 24px; width: 22px; height: 22px;
margin-left: -11px;
animation-name: thSunSway; animation-duration: $dur-loop;
animation-timing-function: ease-in-out; animation-iteration-count: infinite;
animation-direction: alternate;
}
.th__sun {
display: block; width: 22px; height: 22px; border-radius: 50%;
background: radial-gradient(circle, $sunpale 0 58%, rgba(245, 230, 168, 0) 82%);
transition: transform $duration $easing, opacity $duration $easing;
}
.th__mount {
position: absolute; bottom: 14px; width: 46px; height: 30px;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
background: $peak;
}
.th__mount--l { left: -6px; }
.th__mount--r { right: -6px; height: 24px; background: $peak-deep; }
.th__lake {
position: absolute; left: 0; right: 0; bottom: 0; height: 14px;
background: linear-gradient(180deg, $water 0%, #52798c 100%);
}
.th__refl {
position: absolute; left: 50%; bottom: 0; width: 24px; height: 9px;
margin-left: -12px; border-radius: 50%;
background: rgba(245, 230, 168, .62);
animation-name: thShimmer; animation-duration: $dur-loop;
animation-timing-function: ease-in-out; animation-iteration-count: infinite;
animation-direction: alternate;
}
// 모드별 해 높이
.th[data-mode="snow"] .th__sun { transform: translateY(16px); opacity: .45; }
.th[data-mode="sun"] .th__sun { transform: translateY(-8px); opacity: 1; }
.th[data-mode="cloud"] .th__sun { transform: translateY(4px); opacity: .7; }
.th[data-mode="flame"] .th__sun { transform: translateY(0); opacity: .85; }
// ── 하단 조작 패널: 위 (다이얼+온도), 아래 (모드 아이콘 4) 의 두 줄
.th__panel {
position: absolute; left: 16px; right: 16px; bottom: 12px; height: 56px;
border-radius: $r-card;
background: $panel;
display: flex; flex-direction: column; justify-content: center; gap: $sp-1;
padding: 0 $sp-2; box-sizing: border-box;
}
.th__rowtop {
display: flex; align-items: center; gap: $sp-2;
}
.th__dial {
position: relative; width: 26px; height: 26px; flex: 0 0 auto;
border-radius: 50%; background: $case;
transition: transform $duration $easing;
}
.th[data-mode="snow"] .th__dial { transform: rotate(0deg); }
.th[data-mode="sun"] .th__dial { transform: rotate(90deg); }
.th[data-mode="cloud"] .th__dial { transform: rotate(180deg); }
.th[data-mode="flame"] .th__dial { transform: rotate(270deg); }
.th__dialmark {
position: absolute; left: 50%; top: 2px; width: 4px; height: 8px;
margin-left: -2px; border-radius: $r-pill; background: $color;
}
.th__temp {
margin-left: auto;
padding: 0 $sp-2; height: 18px; line-height: 18px;
border-radius: $r-xs; background: $tick-panel;
color: $color; font-weight: 700; font-size: 12px;
letter-spacing: 1px;
}
.th__modes {
display: flex; gap: $sp-1; justify-content: space-between;
}
.th__mode {
width: 20px; height: 20px; padding: 0; border: 0; border-radius: $r-xs;
background: rgba(23, 20, 26, .08); color: #6f7680;
display: inline-flex; align-items: center; justify-content: center;
cursor: pointer; transition: color $duration $easing, background $duration $easing;
}
.th__mode svg { width: 15px; height: 15px; display: block; }
.th__mode[aria-pressed="true"] { color: $color; background: rgba(77, 216, 230, .16); }
.th__hint {
margin: 0; font-size: 11px; line-height: 1.4;
color: rgba(255, 247, 230, .72);
}
@keyframes thSunSway {
from { transform: translateY(0) scale(1); }
to { transform: translateY(9px) scale(1.08); }
}
@keyframes thShimmer {
from { transform: scaleX(1); opacity: .4; }
to { transform: scaleX(1.8); opacity: 1; }
}
// 사람이 고른 상태: 미세 흔들림 루프만 끊고(고른 높이에 머문다), 반사는 계속 일렁
.th:not(.is-demo) .th__sunwrap { animation-name: none; }
@media (max-width: 340px) {
.th__device { width: 124px; height: 124px; box-shadow: 0 0 0 4px $case-edge; }
.th__window { top: 10px; height: 50px; }
.th__panel { height: 46px; }
.th__dial { width: 28px; height: 28px; }
.th__hint { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.th__sunwrap, .th__refl { animation-name: none !important; }
.th__sun, .th__dial, .th__mode { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.th {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: min(100%, 320px);
}
.th__device {
position: relative;
width: 152px;
height: 152px;
border-radius: 50%;
overflow: hidden;
background: #23262c;
box-shadow: 0 0 0 6px #17191e;
}
.th__window {
position: absolute;
left: 16px;
right: 16px;
top: 12px;
height: 62px;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(180deg, #c97e5a 0%, #ebe1d6 100%);
}
.th__sunwrap {
position: absolute;
left: 50%;
top: 24px;
width: 22px;
height: 22px;
margin-left: -11px;
animation-name: thSunSway;
animation-duration: 2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.th__sun {
display: block;
width: 22px;
height: 22px;
border-radius: 50%;
background: radial-gradient(circle, #f5e6a8 0 58%, rgba(245, 230, 168, 0) 82%);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.th__mount {
position: absolute;
bottom: 14px;
width: 46px;
height: 30px;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
background: #5b6067;
}
.th__mount--l {
left: -6px;
}
.th__mount--r {
right: -6px;
height: 24px;
background: #43474d;
}
.th__lake {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 14px;
background: linear-gradient(180deg, #6d94a8 0%, #52798c 100%);
}
.th__refl {
position: absolute;
left: 50%;
bottom: 0;
width: 24px;
height: 9px;
margin-left: -12px;
border-radius: 50%;
background: rgba(245, 230, 168, 0.62);
animation-name: thShimmer;
animation-duration: 2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.th[data-mode=snow] .th__sun {
transform: translateY(16px);
opacity: 0.45;
}
.th[data-mode=sun] .th__sun {
transform: translateY(-8px);
opacity: 1;
}
.th[data-mode=cloud] .th__sun {
transform: translateY(4px);
opacity: 0.7;
}
.th[data-mode=flame] .th__sun {
transform: translateY(0);
opacity: 0.85;
}
.th__panel {
position: absolute;
left: 16px;
right: 16px;
bottom: 12px;
height: 56px;
border-radius: 16px;
background: #fff7e6;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding: 0 8px;
box-sizing: border-box;
}
.th__rowtop {
display: flex;
align-items: center;
gap: 8px;
}
.th__dial {
position: relative;
width: 26px;
height: 26px;
flex: 0 0 auto;
border-radius: 50%;
background: #23262c;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.th[data-mode=snow] .th__dial {
transform: rotate(0deg);
}
.th[data-mode=sun] .th__dial {
transform: rotate(90deg);
}
.th[data-mode=cloud] .th__dial {
transform: rotate(180deg);
}
.th[data-mode=flame] .th__dial {
transform: rotate(270deg);
}
.th__dialmark {
position: absolute;
left: 50%;
top: 2px;
width: 4px;
height: 8px;
margin-left: -2px;
border-radius: 999px;
background: #3ddc97;
}
.th__temp {
margin-left: auto;
padding: 0 8px;
height: 18px;
line-height: 18px;
border-radius: 4px;
background: #231f28;
color: #3ddc97;
font-weight: 700;
font-size: 12px;
letter-spacing: 1px;
}
.th__modes {
display: flex;
gap: 4px;
justify-content: space-between;
}
.th__mode {
width: 20px;
height: 20px;
padding: 0;
border: 0;
border-radius: 4px;
background: rgba(23, 20, 26, 0.08);
color: #6f7680;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), background 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.th__mode svg {
width: 15px;
height: 15px;
display: block;
}
.th__mode[aria-pressed=true] {
color: #3ddc97;
background: rgba(77, 216, 230, 0.16);
}
.th__hint {
margin: 0;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, 0.72);
}
@keyframes thSunSway {
from {
transform: translateY(0) scale(1);
}
to {
transform: translateY(9px) scale(1.08);
}
}
@keyframes thShimmer {
from {
transform: scaleX(1);
opacity: 0.4;
}
to {
transform: scaleX(1.8);
opacity: 1;
}
}
.th:not(.is-demo) .th__sunwrap {
animation-name: none;
}
@media (max-width: 340px) {
.th__device {
width: 124px;
height: 124px;
box-shadow: 0 0 0 4px #17191e;
}
.th__window {
top: 10px;
height: 50px;
}
.th__panel {
height: 46px;
}
.th__dial {
width: 28px;
height: 28px;
}
.th__hint {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.th__sunwrap, .th__refl {
animation-name: none !important;
}
.th__sun, .th__dial, .th__mode {
transition: none !important;
}
}// 모드는 data-mode 하나. 해의 높이·다이얼 각도·온도 숫자가 같이 바뀐다.
// 손이 닿는 순간 미리보기 루프(.is-demo)를 떼고 고른 모드를 유지한다.
(function () {
var root = document.getElementById('thRoot');
var temp = document.getElementById('thTemp');
var buttons = Array.prototype.slice.call(document.querySelectorAll('.th__mode'));
var temps = { snow: '18°', sun: '27°', cloud: '21°', flame: '24°' };
buttons.forEach(function (b) {
b.addEventListener('click', function () {
root.classList.remove('is-demo');
var mode = b.getAttribute('data-mode');
root.setAttribute('data-mode', mode);
buttons.forEach(function (other) {
other.setAttribute('aria-pressed', other === b ? 'true' : 'false');
});
temp.textContent = temps[mode];
});
});
})();// ThermostatScene — vanilla 판(run/078/demo/05-object-thermostat-scene/index.html) 의 React 포트.
// 모드는 data-mode 하나. 해의 높이·다이얼 각도·온도 숫자가 같이 바뀐다.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ThermostatScene.module.scss";
export type ThermostatSceneProps = {
duration?: number;
easing?: string;
color?: string;
initialMode?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const MODE_TEMPS: Record<string, string> = { snow: "18°", sun: "27°", cloud: "21°", flame: "24°" };
const ICONS: Record<string, string> = {
snow: "M220 128a92 92 0 1 1-92-92a92.1 92.1 0 0 1 92 92M223.77 150.09a8 8 0 0 1-5.86 9.68l-24.64 6l6.46 24.11a8 8 0 0 1-5.66 9.8a8.3 8.3 0 0 1-2.07.32a8 8 0 0 1-7.72-5.93l-7.72-28.8L136 141.86v46.83l21.66 21.65a8 8 0 0 1-11.32 11.32L128 203.31l-18.34 18.35a8 8 0 0 1-11.32-11.32L120 188.69v-46.83l-40.55 23.41l-7.72 28.8A8 8 0 0 1 64 200a8.3 8.3 0 0 1-2.08-.27a8 8 0 0 1-5.66-9.8l6.46-24.11l-24.64-6a8 8 0 0 1 3.82-15.54l29.45 7.23L112 128l-40.64-23.46l-29.45 7.23A8 8 0 0 1 40 112a8 8 0 0 1-1.91-15.77l24.64-6l-6.46-24.16a8 8 0 0 1 15.46-4.14l7.72 28.8L120 114.14V67.31L98.34 45.66a8 8 0 0 1 11.32-11.32L128 52.69l18.34-18.35a8 8 0 0 1 11.32 11.32L136 67.31v46.83l40.55-23.41l7.72-28.8a8 8 0 0 1 15.46 4.14l-6.46 24.11l24.64 6A8 8 0 0 1 216 112a8 8 0 0 1-1.91-.23l-29.45-7.23L144 128l40.64 23.46l29.45-7.23a8 8 0 0 1 9.68 5.86",
sun: "M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m72 88a64 64 0 1 1-64-64a64.07 64.07 0 0 1 64 64m-16 0a48 48 0 1 0-48 48a48.05 48.05 0 0 0 48-48M58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16",
cloud: "M240 128a80 80 0 0 1-80 80H72A56 56 0 1 1 85.92 97.74v.1A80 80 0 0 1 240 128M160 40a88.09 88.09 0 0 0-78.71 48.67A64 64 0 1 0 72 216h88a88 88 0 0 0 0-176m0 160H72a48 48 0 0 1 0-96c1.1 0 2.2 0 3.29.11A88 88 0 0 0 72 128a8 8 0 0 0 16 0a72 72 0 1 1 72 72",
flame: "M208 144a80 80 0 0 1-77.94 80A40 40 0 0 0 168 184c0-40-40-64-40-64s-40 24-40 64a40 40 0 0 0 37.94 40A80 80 0 0 1 48 144c0-72 80-120 80-120s80 48 80 120M173.79 51.48a221.3 221.3 0 0 0-41.67-34.34a8 8 0 0 0-8.24 0a221.3 221.3 0 0 0-41.67 34.34C54.59 80.48 40 112.47 40 144a88 88 0 0 0 176 0c0-31.53-14.59-63.52-42.21-92.52M96 184c0-27.67 22.53-47.28 32-54.3c9.48 7 32 26.63 32 54.3a32 32 0 0 1-64 0m77.27 15.93A47.8 47.8 0 0 0 176 184c0-44-42.09-69.79-43.88-70.86a8 8 0 0 0-8.24 0C122.09 114.21 80 140 80 184a47.8 47.8 0 0 0 2.73 15.93A71.88 71.88 0 0 1 56 144c0-34.41 20.4-63.15 37.52-81.19A216.2 216.2 0 0 1 128 33.54a215.8 215.8 0 0 1 34.48 29.27C193.49 95.5 200 125 200 144a71.88 71.88 0 0 1-26.73 55.93",
};
const DOTS: Record<string, string> = {
snow: "M220 128a92 92 0 1 1-92-92a92.1 92.1 0 0 1 92 92",
sun: "M184 128a56 56 0 1 1-56-56a56 56 0 0 1 56 56",
cloud: "M240 128a80 80 0 0 1-80 80H72A56 56 0 1 1 85.92 97.74v.1A80 80 0 0 1 240 128",
flame: "M208 144a80 80 0 0 1-77.94 80A40 40 0 0 0 168 184c0-40-40-64-40-64s-40 24-40 64a40 40 0 0 0 37.94 40A80 80 0 0 1 48 144c0-72 80-120 80-120s80 48 80 120",
};
const MODE_LABELS: Record<string, string> = { snow: "눈 모드", sun: "해 모드", cloud: "구름 모드", flame: "난방 모드" };
export default function ThermostatScene(props: ThermostatSceneProps) {
const duration = props.duration ?? 300;
const easing = props.easing ?? "cubic-bezier(.2, .8, .2, 1)";
const color = props.color ?? "#3ddc97";
const [mode, setMode] = useState<string>(props.initialMode ?? "sun");
const [preview, setPreview] = useState<boolean>(true);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
const rootClass = ["th", preview ? "is-demo" : ""]
.filter(Boolean).map((c) => (styles as { [k: string]: string })[c]).join(" ");
return (
<main className={styles.stage}>
<div className={rootClass} style={vars} data-mode={mode}>
<div className={styles["th__device"]}>
<div className={styles["th__window"]}>
<span className={styles["th__sky"]}></span>
<span className={styles["th__sunwrap"]}><span className={styles["th__sun"]}></span></span>
<span className={styles["th__mount"] + " " + styles["th__mount--l"]}></span>
<span className={styles["th__mount"] + " " + styles["th__mount--r"]}></span>
<span className={styles["th__lake"]}><span className={styles["th__refl"]}></span></span>
</div>
<div className={styles["th__panel"]}>
<div className={styles["th__rowtop"]}>
<span className={styles["th__dial"]}><span className={styles["th__dialmark"]}></span></span>
<span className={styles["th__temp"]}>{MODE_TEMPS[mode]}</span>
</div>
<div className={styles["th__modes"]}>
{["snow", "sun", "cloud", "flame"].map((m) => (
<button
key={m}
className={styles["th__mode"]}
type="button"
aria-pressed={mode === m ? "true" : "false"}
aria-label={MODE_LABELS[m]}
onClick={() => { setPreview(false); setMode(m); }}
>
<svg viewBox="0 0 256 256" aria-hidden="true">
<g fill="currentColor">
<path d={DOTS[m]} opacity=".2"></path>
<path d={ICONS[m]}></path>
</g>
</svg>
</button>
))}
</div>
</div>
</div>
<p className={styles["th__hint"]}>눈·해·구름·난방 아이콘을 눌러 보세요</p>
</div>
</main>
);
}05 サーモスタットの風景窓 — 計測 累積面積 0.536% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · CSS アニメーション サンプル 9選 — 洗濯機・家・車を図形で動かす
このデモが属する記事
CSS アニメーション サンプル 9選 — 洗濯機・家・車を図形で動かすcss アニメーション サンプルとして、洗濯機・家・車などの身の回りの物を、画像を一枚も使わずに色付きの箱いくつかで描いて動かす作り方を9つまとめました。どれも純粋な CSS の図形だけで、下のグリッドの中で今動いています。 この記事のデモをすべて見る


