09The hero scene — day-night lighting and parallax turning together
Transitioning the light-angle variable from a day value to a night value recomputes item 03's shading formula unchanged, and day flips to night. Layer in item 05's pointer parallax and the demo closes as one hero banner you can tilt and re-light at once.
Best forLanding heroes, brand banners
CSS custom propertiestransitionpointermove

<main class="stage">
<div class="dn" id="dn">
<p class="dn__hint">The pointer's vertical position sets the day-night progress (t) — sky and windows interpolate by t every frame, while sun→moon swaps via a transition</p>
<div class="dn__field" role="application" tabindex="0" aria-label="Scene where moving the pointer up and down turns an isometric town from day to night" id="field">
<div class="dn__sky" id="sky">
<div class="dn__orb dn__orb--sun" id="sun"></div>
<div class="dn__orb dn__orb--moon" id="moon"></div>
</div>
<div class="dn__world" id="world"></div>
</div>
<p class="dn__meta">Night progress <b id="pct">0</b>%</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 낮밤 히어로(09) — 무대는 먹. 하늘색·창문 밝기는 JS 가 낮밤 진행도(t)로 매 프레임 직접
// 보간해 칠한다(transition 없이 매끄럽다). 해→달 교체만 t=0.5 문턱에서 is-night 클래스로
// 진짜 opacity transition 크로스페이드를 튼다.
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-blue-light;
@include stage($stage-ink);
.dn {
display: flex; flex-direction: column; align-items: center; gap: $sp-2;
width: 100%; max-width: 420px;
}
.dn__hint {
margin: 0; font-weight: 700; font-size: clamp(10px, 2.6vw, 12px); line-height: 1.35;
color: $subject-cream; text-align: center;
}
.dn__field {
position: relative;
width: clamp(180px, 42vw, 220px); height: clamp(140px, 34vw, 168px);
display: grid; place-items: center;
border-radius: $r-sm;
overflow: hidden;
outline: none;
}
.dn__field:focus-visible { box-shadow: inset 0 0 0 2px rgba($subject-cream, .6); }
.dn__sky {
position: absolute; inset: 0;
background-color: $subject-blue-light;
border-radius: $r-sm;
}
.dn__orb {
position: absolute; left: 50%; top: 50%;
width: 16px; height: 16px; border-radius: $r-pill;
margin-left: -8px; margin-top: -8px;
transition: opacity $duration $easing;
}
.dn__orb--sun { background: $stage-yellow; box-shadow: 0 0 10px rgba($stage-yellow, .8); opacity: 1; }
.dn__orb--moon { background: $subject-cream; box-shadow: 0 0 8px rgba($subject-cream, .7); opacity: 0; }
.dn__field.is-night .dn__orb--sun { opacity: 0; }
.dn__field.is-night .dn__orb--moon { opacity: 1; }
.dn__world { position: relative; transform-style: preserve-3d; width: 1px; height: 1px; }
.dn__bld {
position: absolute; left: 0; top: 0;
width: 34px; height: 34px;
transform: translate3d(var(--tx), var(--ty), 0) translate(-50%, -50%);
background: $subject-ink;
border: 1px solid rgba($subject-cream, .2);
border-radius: $r-xs;
box-shadow: $shadow-press;
}
.dn__win {
position: absolute; left: 50%; top: 50%;
width: 6px; height: 6px;
transform: translate(-50%, -50%);
background: $stage-yellow;
border-radius: $r-xs;
}
.dn__meta { margin: 0; font-size: clamp(9px, 2.4vw, 11px); color: rgba($subject-cream, .82); }
.dn__meta b { font-weight: 800; color: $stage-yellow; font-variant-numeric: tabular-nums; }
@media (max-width: 340px) {
.dn { gap: $sp-1; }
.dn__field { width: 156px; height: 112px; }
.dn__hint { font-size: 8px; }
.dn__meta { font-size: 7px; }
}
// reduce — 포인터 추적·자동 순환이 멎는다. 0.6(밤 쪽) 진행도로 고정된 정지 장면만 남긴다.
@media (prefers-reduced-motion: reduce) {
.dn.is-still .dn__orb { transition: none; }
}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);
}
.dn {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
max-width: 420px;
}
.dn__hint {
margin: 0;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
line-height: 1.35;
color: #fff7e6;
text-align: center;
}
.dn__field {
position: relative;
width: clamp(180px, 42vw, 220px);
height: clamp(140px, 34vw, 168px);
display: grid;
place-items: center;
border-radius: 8px;
overflow: hidden;
outline: none;
}
.dn__field:focus-visible {
box-shadow: inset 0 0 0 2px rgba(255, 247, 230, 0.6);
}
.dn__sky {
position: absolute;
inset: 0;
background-color: #5b8cf8;
border-radius: 8px;
}
.dn__orb {
position: absolute;
left: 50%;
top: 50%;
width: 16px;
height: 16px;
border-radius: 999px;
margin-left: -8px;
margin-top: -8px;
transition: opacity 300ms ease-out;
}
.dn__orb--sun {
background: #ffd23f;
box-shadow: 0 0 10px rgba(255, 210, 63, 0.8);
opacity: 1;
}
.dn__orb--moon {
background: #fff7e6;
box-shadow: 0 0 8px rgba(255, 247, 230, 0.7);
opacity: 0;
}
.dn__field.is-night .dn__orb--sun {
opacity: 0;
}
.dn__field.is-night .dn__orb--moon {
opacity: 1;
}
.dn__world {
position: relative;
transform-style: preserve-3d;
width: 1px;
height: 1px;
}
.dn__bld {
position: absolute;
left: 0;
top: 0;
width: 34px;
height: 34px;
transform: translate3d(var(--tx), var(--ty), 0) translate(-50%, -50%);
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.2);
border-radius: 4px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.dn__win {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
transform: translate(-50%, -50%);
background: #ffd23f;
border-radius: 4px;
}
.dn__meta {
margin: 0;
font-size: clamp(9px, 2.4vw, 11px);
color: rgba(255, 247, 230, 0.82);
}
.dn__meta b {
font-weight: 800;
color: #ffd23f;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.dn {
gap: 4px;
}
.dn__field {
width: 156px;
height: 112px;
}
.dn__hint {
font-size: 8px;
}
.dn__meta {
font-size: 7px;
}
}
@media (prefers-reduced-motion: reduce) {
.dn.is-still .dn__orb {
transition: none;
}
}// 낮밤 히어로(09) — 포인터 세로 위치 → 낮밤 진행도 t(0~1). 하늘색·창문 밝기는 t 로
// 매 프레임 직접 보간(JS lerp, transition 없이 매끄럽다). 해↔달 교체만 t=0.5 문턱에서
// 클래스를 토글해 진짜 CSS transition(opacity)으로 크로스페이드한다. 입력이 없으면
// requestAnimationFrame 루프가 자체 경로로 낮밤을 순환시킨다(자동 시연).
(function () {
var root = document.getElementById('dn');
var field = document.getElementById('field');
var sky = document.getElementById('sky');
var world = document.getElementById('world');
var sun = document.getElementById('sun');
var moon = document.getElementById('moon');
var pctEl = document.getElementById('pct');
if (!root || !field || !sky || !world || !sun || !moon || !pctEl) return;
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); }
function lerp(a, b, t) { return a + (b - a) * t; }
function mat4RotateX(deg) {
var r = deg * Math.PI / 180, c = Math.cos(r), s = Math.sin(r);
return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
}
function mat4RotateZ(deg) {
var r = deg * Math.PI / 180, c = Math.cos(r), s = Math.sin(r);
return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
}
function mat4Mul(a, b) {
var out = new Array(16);
for (var col = 0; col < 4; col++) {
for (var row = 0; row < 4; row++) {
var sum = 0;
for (var k = 0; k < 4; k++) sum += a[k * 4 + row] * b[col * 4 + k];
out[col * 4 + row] = sum;
}
}
return out;
}
function mat4ToCss(m) {
var s = '';
for (var i = 0; i < m.length; i++) s += (i ? ',' : '') + m[i].toFixed(6);
return 'matrix3d(' + s + ')';
}
world.style.transform = mat4ToCss(mat4Mul(mat4RotateX(-35.264), mat4RotateZ(45)));
var mm = window.matchMedia('(max-width: 340px)').matches;
var N = 2;
var TILE_W = mm ? 32 : 40, TILE_H = mm ? 16 : 20;
var wins = [];
for (var row = 0; row < N; row++) {
for (var col = 0; col < N; col++) {
var x = (col - row) * TILE_W / 2;
var y = (col + row) * TILE_H / 2;
var bld = document.createElement('div');
bld.className = 'dn__bld';
bld.style.setProperty('--tx', x.toFixed(2) + 'px');
bld.style.setProperty('--ty', y.toFixed(2) + 'px');
bld.style.zIndex = String(Math.round((col + row) * 10));
var win = document.createElement('div');
win.className = 'dn__win';
bld.appendChild(win);
world.appendChild(bld);
wins.push(win);
}
}
// 낮 하늘색 → 밤 하늘색 (RGB 보간, day.length === night.length)
var DAY = [91, 140, 248]; // $subject-blue-light 근사
var NIGHT = [23, 20, 26]; // $stage-ink
var targetT = 0, curT = 0;
var lastInput = -Infinity;
var IDLE_MS = 1100;
field.addEventListener('pointermove', function (ev) {
var rect = field.getBoundingClientRect();
var ny = (ev.clientY - rect.top) / rect.height;
targetT = clamp(ny, 0, 1);
lastInput = performance.now();
});
field.addEventListener('pointerleave', function () { lastInput = -Infinity; });
var wasNight = false;
function paint(now, t) {
if (now - lastInput > IDLE_MS) targetT = (1 - Math.cos(t * 3)) / 2;
curT += (targetT - curT) * 0.14;
var r = Math.round(lerp(DAY[0], NIGHT[0], curT));
var g = Math.round(lerp(DAY[1], NIGHT[1], curT));
var b = Math.round(lerp(DAY[2], NIGHT[2], curT));
sky.style.backgroundColor = 'rgb(' + r + ',' + g + ',' + b + ')';
for (var i = 0; i < wins.length; i++) {
wins[i].style.opacity = String((0.15 + curT * 0.85).toFixed(2));
}
var isNight = curT > 0.5;
if (isNight !== wasNight) {
field.classList.toggle('is-night', isNight);
wasNight = isNight;
}
var orbY = mm ? 60 : 76;
var ox = (curT - 0.5) * (mm ? 120 : 150);
var oy = -Math.cos(curT * Math.PI) * orbY;
sun.style.transform = 'translate3d(' + ox.toFixed(1) + 'px,' + oy.toFixed(1) + 'px,0)';
moon.style.transform = 'translate3d(' + ox.toFixed(1) + 'px,' + oy.toFixed(1) + 'px,0)';
pctEl.textContent = String(Math.round(curT * 100));
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) {
targetT = 0.6; curT = 0.6;
paint(0, 0.3);
root.classList.add('is-still');
} else {
var start = performance.now();
(function step(now) {
paint(now, (now - start) / 1000);
requestAnimationFrame(step);
})(start);
}
})();// IsometricHeroDayNight — vanilla 판(run/333/demo/09-isometric-hero-day-night/index.html)
// 의 React 포트. 포인터 세로 위치가 낮밤 진행도(t)를 정한다 — 하늘·창문 색은 t 로 매
// 프레임 RGB 보간하고, 해→달은 t>0.5 에서 클래스 토글로 한 프레임에 컷 교체한다.
import { useEffect, useRef } from "react";
import styles from "./IsometricHeroDayNight.module.scss";
export type IsometricHeroDayNightProps = {
duration?: string;
easing?: string;
color?: string;
};
type Box<T> = { current: T };
const HINT = "포인터 세로 위치가 낮밤 진행도(t)를 정합니다 — 하늘·창문은 t 로 매 프레임 보간, 해→달은 transition 으로 교체됩니다";
const ARIA_FIELD = "포인터를 위아래로 움직이면 등각 마을이 낮에서 밤으로 바뀌는 장면";
const LABEL_NIGHT = "밤 진행도 ";
const SKY_DAY = [110, 200, 255];
const SKY_NIGHT = [23, 20, 26];
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
function mixRgb(a: number[], b: number[], t: number): string {
return (
"rgb(" +
Math.round(lerp(a[0], b[0], t)) +
"," +
Math.round(lerp(a[1], b[1], t)) +
"," +
Math.round(lerp(a[2], b[2], t)) +
")"
);
}
export default function IsometricHeroDayNight({
duration = "600ms",
easing = "ease-out",
color = "#5b8cf8"
}: IsometricHeroDayNightProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const fieldRef = useRef<HTMLDivElement | null>(null);
const skyRef = useRef<HTMLDivElement | null>(null);
const nightPctRef = useRef<HTMLElement | null>(null);
const stateRef = useRef({ targetT: 0.2, curT: 0.2 }) as Box<{ targetT: number; curT: number }>;
const rafRef = useRef(0) as Box<number>;
useEffect(() => {
const field = fieldRef.current;
const sky = skyRef.current;
const root = rootRef.current;
if (!field || !sky || !root) return;
function onMove(e: PointerEvent): void {
if (!field) return;
const rect = field.getBoundingClientRect();
const ny = (e.clientY - rect.top) / rect.height;
stateRef.current.targetT = ny < 0 ? 0 : ny > 1 ? 1 : ny;
}
field.addEventListener("pointermove", onMove);
function paint(): void {
const s = stateRef.current;
s.curT += (s.targetT - s.curT) * 0.12;
sky!.style.background = mixRgb(SKY_DAY, SKY_NIGHT, s.curT);
const isNight = s.curT > 0.5;
field!.classList.toggle(styles.isNight, isNight);
if (nightPctRef.current) nightPctRef.current.textContent = String(Math.round(s.curT * 100));
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
stateRef.current.curT = 0.5;
stateRef.current.targetT = 0.5;
paint();
root.classList.add(styles.isStill);
} else {
const step = (): void => {
paint();
rafRef.current = requestAnimationFrame(step);
};
rafRef.current = requestAnimationFrame(step);
}
return () => {
cancelAnimationFrame(rafRef.current);
field.removeEventListener("pointermove", onMove);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<main className={styles.stage}>
<div className={styles.hn} ref={rootRef} data-duration={duration} data-easing={easing} data-color={color}>
<p className={styles.hint}>{HINT}</p>
<div className={styles.field} ref={fieldRef} role="img" aria-label={ARIA_FIELD} tabIndex={0}>
<div className={styles.sky} ref={skyRef}></div>
<div className={styles.sun}></div>
<div className={styles.moon}></div>
<div className={styles.town}>
<div className={styles.house}></div>
<div className={styles.house}></div>
<div className={styles.house}></div>
</div>
</div>
<p className={styles.meta}>
<span className={styles.metaLabel}>{LABEL_NIGHT}</span>
<b className={styles.nightVal} ref={nightPctRef}>20</b>%
</p>
</div>
</main>
);
}09 The hero scene — day-night lighting and parallax turning together — Measured area 21.418% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Isometric CSS: 9 True-Projection 3D Dioramas
The post this demo lives in
Isometric CSS: 9 True-Projection 3D DioramasIsometric css done right: Each cube face shades by a light-angle calc, grid coords map to screen via the 2:1 isometric f See all demos in this post
If you like this effect - similar demos
Interactions09 A Meadow Scene Where Fourteen Bubbles Ride the Wind, Each Computing Its Own Color and ReflectionBubble Animation CSS: 9 Soap-Film Scenes That Pop
Interactions08 Fallback that hands over to the tilt sensor3D Hover Effect CSS: 9 Pointer-Following Scenes
Interactions01 Follow basics — the lagged chaseMouse Follow Effect CSS — 9 Cursor Chasers