05アイザワ・トーマス — 球面と螺旋、別の方程式がくれた別の彫刻
アトラクターはローレンツだけではありません。球をまとうアイザワ、糸巻き螺旋のトーマス。係数を少し変えるだけで形が一変することを見せる比較実験項目です。
使いどころアートヒーロー、展示型背景
requestAnimationFrameMath.sinlineTo

<main class="stage">
<div class="lz is-demo" id="lzRoot">
<p class="lz__hint">
<span>係数をいくつか変えた別の微分方程式は、球をまとうアイザワと糸巻き螺旋のトーマスを描きます</span>
</p>
<div class="lz__field" role="application" aria-label="アイザワ・トーマスの実験盤" tabindex="0">
<canvas class="lz__cv" id="lzCv"></canvas>
</div>
<div class="lz__bar">
<span class="lz__meta">左アイザワ · 右トーマス · RK4</span>
<span class="lz__meta lz__meta--dim">canvas · 奥行きシェーディング · 2秒周期</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 아이자와·토머스(05) — @keyframes 를 두지 않는다.
// 움직임은 캔버스 rAF 루프(두 미분방정식을 RK4 로 적분해 나란히 드러낸다)가 만든다.
// 무대는 편 공통 심해 남색. 아이자와=라일락 화이트, 토머스=민트 크림.
$lz-stage: #16202b;
@include stage($lz-stage);
$lz-panel: #0d1622;
$lz-text: #ccd8e6;
$lz-ca: #e6d9ff; // 아이자와 궤적 — 라일락 화이트
$lz-ct: #d8f0e3; // 토머스 궤적 — 민트 크림
$duration: $dur-loop;
$easing: $ease-out;
$color: $lz-ca;
.lz {
--lz-panel: #{$lz-panel};
--lz-ca: #{$lz-ca};
--lz-ct: #{$lz-ct};
--lz-head: #{$subject-cream};
display: flex; flex-direction: column; gap: $sp-2;
width: 100%; max-width: 424px;
}
.lz__hint {
display: flex; align-items: center; gap: $sp-1; margin: 0;
font-weight: 700; font-size: clamp(10px, 2.8vw, 12px); line-height: 1.35;
color: $lz-text;
}
.lz__field {
position: relative; height: 180px;
border-radius: $r-card; overflow: hidden;
background: $lz-panel;
box-shadow: inset 0 0 0 1px rgba($lz-text, .18);
}
.lz__field:focus-visible { outline: 2px solid $lz-ca; outline-offset: 2px; }
.lz__cv { display: block; width: 100%; height: 100%; }
.lz__bar { display: flex; align-items: baseline; justify-content: space-between; gap: $sp-2; }
.lz__meta { font-size: 10px; font-weight: 500; color: rgba($lz-text, .72); }
.lz__meta--dim { font-size: 10px; }
@media (max-width: 340px) {
.lz { gap: $sp-1; }
.lz__field { height: 100px; }
.lz__meta--dim { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.lz.is-still .lz__field { cursor: default !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #16202b;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.lz {
--lz-panel: #0d1622;
--lz-ca: #e6d9ff;
--lz-ct: #d8f0e3;
--lz-head: #fff7e6;
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 424px;
}
.lz__hint {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 12px);
line-height: 1.35;
color: #ccd8e6;
}
.lz__field {
position: relative;
height: 180px;
border-radius: 16px;
overflow: hidden;
background: #0d1622;
box-shadow: inset 0 0 0 1px rgba(204, 216, 230, 0.18);
}
.lz__field:focus-visible {
outline: 2px solid #e6d9ff;
outline-offset: 2px;
}
.lz__cv {
display: block;
width: 100%;
height: 100%;
}
.lz__bar {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.lz__meta {
font-size: 10px;
font-weight: 500;
color: rgba(204, 216, 230, 0.72);
}
.lz__meta--dim {
font-size: 10px;
}
@media (max-width: 340px) {
.lz {
gap: 4px;
}
.lz__field {
height: 100px;
}
.lz__meta--dim {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.lz.is-still .lz__field {
cursor: default !important;
}
}// 아이자와·토머스(05) — 로렌츠가 아닌 어트랙터 두 개를 나란히 보여준다.
// 아이자와(구면): dx = (z−b)x − d·y, dy = d·x + (z−b)y, dz = c + a·z − z³/3 − (x²+y²)(1+e·z) + f·z·x³
// 토머스(나선): dx = sin(y) − b·x, dy = sin(z) − b·y, dz = sin(x) − b·z
// 두 계통 모두 RK4 로 적분하고, 관찰축을 천천히 돌려 입체 형태를 보여준다.
(function () {
// ── 변형 상수 블록 — 아래 값 하나만 바꿔 변형을 만든다 ──
var DT = 0.02; // RK4 걸음
var STEPS = 1800; // 각 판의 사전 적분 걸음 수
var SPIN_S = 2.0; // 궤적 드러나기 주기(초) — 설정 초(2s)와 같다
var SPIN_L = 12.0; // 관찰축 회전 주기(초)
// ────────────────────────────────────────────────────
var root = document.getElementById('lzRoot');
var cvEl = document.getElementById('lzCv');
if (!root || !cvEl) return;
var fieldEl = cvEl.parentElement;
var css = getComputedStyle(root);
function tokRGB(name) {
var s = css.getPropertyValue(name).trim();
var hx = s.length === 4
? s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2) + s.charAt(3) + s.charAt(3)
: s.slice(1);
return [parseInt(hx.slice(0, 2), 16), parseInt(hx.slice(2, 4), 16), parseInt(hx.slice(4, 6), 16)];
}
var CA = tokRGB('--lz-ca'); // 아이자와 — 라일락 화이트
var CT = tokRGB('--lz-ct'); // 토머스 — 민트 크림
var HEAD = tokRGB('--lz-head'); // 머리 — 크림(토큰)
var PANEL = tokRGB('--lz-panel');
function rgba(c, a) { return 'rgba(' + (c[0] | 0) + ',' + (c[1] | 0) + ',' + (c[2] | 0) + ',' + a + ')'; }
// 아이자와 계수 — a=0.95, b=0.7, c=0.6, d=3.5, e=0.25, f=0.1 의 대표 형태
function dAizawa(x, y, z) {
var a = 0.95, b = 0.7, c = 0.6, d = 3.5, e = 0.25, f = 0.1;
return [
(z - b) * x - d * y,
d * x + (z - b) * y,
c + a * z - z * z * z / 3 - (x * x + y * y) * (1 + e * z) + f * z * x * x * x
];
}
// 토머스 계수 b=0.19 — 주기적으로 겹는 나선
function dThomas(x, y, z) {
var b = 0.19;
return [Math.sin(y) - b * x, Math.sin(z) - b * y, Math.sin(x) - b * z];
}
function rk4(p, f) {
var k1 = f(p[0], p[1], p[2]);
var k2 = f(p[0] + k1[0] * DT / 2, p[1] + k1[1] * DT / 2, p[2] + k1[2] * DT / 2);
var k3 = f(p[0] + k2[0] * DT / 2, p[1] + k2[1] * DT / 2, p[2] + k2[2] * DT / 2);
var k4 = f(p[0] + k3[0] * DT, p[1] + k3[1] * DT, p[2] + k3[2] * DT);
return [
p[0] + DT / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]),
p[1] + DT / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]),
p[2] + DT / 6 * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2])
];
}
// 사전 적분 — 아이자와는 구 중심 근처, 토머스는 원점에서
var azX = new Float32Array(STEPS), azY = new Float32Array(STEPS), azZ = new Float32Array(STEPS);
var thX = new Float32Array(STEPS), thY = new Float32Array(STEPS), thZ = new Float32Array(STEPS);
var pa = [0.1, 0, 0], pt = [1.1, 1.2, -0.6], i;
for (i = 0; i < 100; i++) { pa = rk4(pa, dAizawa); pt = rk4(pt, dThomas); }
for (i = 0; i < STEPS; i++) {
azX[i] = pa[0]; azY[i] = pa[1]; azZ[i] = pa[2]; pa = rk4(pa, dAizawa);
thX[i] = pt[0]; thY[i] = pt[1]; thZ[i] = pt[2]; pt = rk4(pt, dThomas);
}
var ctx = cvEl.getContext('2d');
var W = 0, H = 0;
function fit() {
var r = fieldEl.getBoundingClientRect();
W = Math.round(r.width); H = Math.round(r.height);
cvEl.width = W; cvEl.height = H;
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.font = '600 10px -apple-system, "Segoe UI", sans-serif';
}
var REVEAL_END = 0.82;
// 한 판 그리기 — xz 투영 + y 깊이 음영. xz: x 가로, z 세로, y 를 사선 성분으로
function drawPane(X, Y, Z, cx, cy, s, col, n, fade, tilt) {
if (n < 2) return;
var i2, hx, hy;
// 뒤네루게
ctx.strokeStyle = rgba(col, 0.12 * fade);
ctx.lineWidth = 3.0;
ctx.beginPath();
hx = cx + (X[0] + Y[0] * tilt) * s; hy = cy - Z[0] * s;
ctx.moveTo(hx, hy);
for (i2 = 1; i2 < n; i2++) ctx.lineTo(cx + (X[i2] + Y[i2] * tilt) * s, cy - Z[i2] * s);
ctx.stroke();
// 알맹이 — 걸음 진행에 따라 밝아지는 램프
var ramp = Math.max(0.10, 0.5 * (1 - n / STEPS) + 0.15);
ctx.strokeStyle = rgba(col, ramp * fade);
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(cx + (X[0] + Y[0] * tilt) * s, cy - Z[0] * s);
for (i2 = 1; i2 < n; i2++) ctx.lineTo(cx + (X[i2] + Y[i2] * tilt) * s, cy - Z[i2] * s);
ctx.stroke();
// 머리
hx = cx + (X[n - 1] + Y[n - 1] * tilt) * s; hy = cy - Z[n - 1] * s;
ctx.fillStyle = rgba(HEAD, 0.22 * fade);
ctx.beginPath(); ctx.arc(hx, hy, 5, 0, 6.2832); ctx.fill();
ctx.fillStyle = rgba(HEAD, fade);
ctx.beginPath(); ctx.arc(hx, hy, 2, 0, 6.2832); ctx.fill();
}
function draw(phase, tSec) {
ctx.fillStyle = rgba(PANEL, 1);
ctx.fillRect(0, 0, W, H);
var upto = Math.min(1, phase / REVEAL_END);
var n = Math.floor(upto * STEPS);
var fade = phase > REVEAL_END ? Math.max(0, 1 - (phase - REVEAL_END) / (1 - REVEAL_END)) : 1;
// 관찰축 회전 — 사선 성분 tilt 를 진동시킨다
var tilt = Math.sin(tSec / SPIN_L * 6.2832) * 0.5;
var mid = W / 2;
ctx.strokeStyle = rgba(CA, 0.14);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(mid, 0); ctx.lineTo(mid, H); ctx.stroke();
drawPane(azX, azY, azZ, mid * 0.52, H * 0.52, Math.min(W / 46, H / 42) * 0.6, CA, n, fade, tilt);
drawPane(thX, thY, thZ, mid * 1.48, H * 0.5, Math.min(W / 46, H / 42) * 0.5, CT, n, fade, tilt);
ctx.textAlign = 'center';
ctx.fillStyle = rgba(CA, 0.9 * fade + 0.1);
ctx.fillText('AIZAWA', mid * 0.52, H - 6);
ctx.fillStyle = rgba(CT, 0.9 * fade + 0.1);
ctx.fillText('THOMAS', mid * 1.48, H - 6);
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var t0 = -1;
function frame(now) {
if (t0 < 0) t0 = now;
var tSec = (now - t0) / 1000;
draw((tSec % SPIN_S) / SPIN_S, tSec);
requestAnimationFrame(frame);
}
window.addEventListener('resize', function () {
fit();
if (reduce) draw(0.62, 0.4);
});
fit();
if (reduce) {
draw(0.62, 0.4);
root.classList.add('is-still');
} else {
requestAnimationFrame(frame);
}
})();// LorenzAtlasAizawaThomas — vanilla 판(run/324/demo/05-lorenz-atlas-aizawa-thomas/index.html) 의 React 포트.
// 아이자와(구면): dx = (z−b)x − d·y, dy = d·x + (z−b)y, dz = c + a·z − z³/3 − (x²+y²)(1+e·z) + f·z·x³
// 토머스(나선): dx = sin(y) − b·x, dy = sin(z) − b·y, dz = sin(x) − b·z
// 두 계통 모두 RK4 로 적분하고, 관찰축을 천천히 돌려 입체 형태를 보여준다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./LorenzAtlasAizawaThomas.module.scss";
export type LorenzAtlasAizawaThomasProps = {
duration?: string;
easing?: string;
color?: string;
steps?: number; // 각 판의 사전 적분 걸음 수
spinS?: number; // 궤적 드러나기 주기(초) — 설정 초(2s)와 같다
};
// ── 변형 상수(기본값) — vanilla 판과 동일 ──
const DT = 0.02; // RK4 걸음
const STEPS = 1800; // 각 판의 사전 적분 걸음 수
const SPIN_S = 2.0; // 궤적 드러나기 주기(초) — 설정 초(2s)와 같다
const SPIN_L = 12.0; // 관찰축 회전 주기(초)
const REVEAL_END = 0.82;
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Box<T> = { current: T };
type RGB = [number, number, number];
type Vec3 = [number, number, number];
type Deriv = (x: number, y: number, z: number) => Vec3;
const FIELD_LABEL = "아이자와·토머스 실험판";
const META_LEFT = "좌 아이자와 · 우 토머스 · RK4";
const META_RIGHT = "canvas · 깊이 음영 · 주기 2초";
const HINT = "계수 몇 개를 바꾼 또 다른 미분방정식은 구면을 감도는 아이자와와 실뭉치 나선의 토머스를 그립니다";
// 아이자와 계수 — a=0.95, b=0.7, c=0.6, d=3.5, e=0.25, f=0.1 의 대표 형태
function dAizawa(x: number, y: number, z: number): Vec3 {
const a = 0.95, b = 0.7, c = 0.6, d = 3.5, e = 0.25, f = 0.1;
return [
(z - b) * x - d * y,
d * x + (z - b) * y,
c + a * z - z * z * z / 3 - (x * x + y * y) * (1 + e * z) + f * z * x * x * x
];
}
// 토머스 계수 b=0.19 — 주기적으로 겹는 나선
function dThomas(x: number, y: number, z: number): Vec3 {
const b = 0.19;
return [Math.sin(y) - b * x, Math.sin(z) - b * y, Math.sin(x) - b * z];
}
function rk4(p: Vec3, f: Deriv): Vec3 {
const k1 = f(p[0], p[1], p[2]);
const k2 = f(p[0] + k1[0] * DT / 2, p[1] + k1[1] * DT / 2, p[2] + k1[2] * DT / 2);
const k3 = f(p[0] + k2[0] * DT / 2, p[1] + k2[1] * DT / 2, p[2] + k2[2] * DT / 2);
const k4 = f(p[0] + k3[0] * DT, p[1] + k3[1] * DT, p[2] + k3[2] * DT);
return [
p[0] + DT / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]),
p[1] + DT / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]),
p[2] + DT / 6 * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2])
];
}
function rgba(c: RGB, a: number): string {
return "rgba(" + (c[0] | 0) + "," + (c[1] | 0) + "," + (c[2] | 0) + "," + a + ")";
}
export default function LorenzAtlasAizawaThomas({
duration = "2s",
easing = "ease-out",
color = "#e6d9ff",
steps = STEPS,
spinS = SPIN_S
}: LorenzAtlasAizawaThomasProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const fieldRef = useRef<HTMLDivElement | null>(null);
const cvRef = useRef<HTMLCanvasElement | null>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const caRef = useRef<RGB | null>(null);
const ctRef = useRef<RGB | null>(null);
const headRef = useRef<RGB | null>(null);
const panelRef = useRef<RGB | null>(null);
const azRef = useRef<{ x: Float32Array; y: Float32Array; z: Float32Array } | null>(null);
const thRef = useRef<{ x: Float32Array; y: Float32Array; z: Float32Array } | null>(null);
const wRef = useRef(0) as Box<number>;
const hRef = useRef(0) as Box<number>;
const rafRef = useRef(0) as Box<number>;
const [still, setStill] = useState(false);
const vars: Vars = { "--duration": duration, "--easing": easing, "--color": color };
function reduced(): boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function tokRGB(root: HTMLDivElement, name: string): RGB {
const s = getComputedStyle(root).getPropertyValue(name).trim();
const hx = s.length === 4
? s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2) + s.charAt(3) + s.charAt(3)
: s.slice(1);
return [parseInt(hx.slice(0, 2), 16), parseInt(hx.slice(2, 4), 16), parseInt(hx.slice(4, 6), 16)];
}
function fit(): void {
const field = fieldRef.current;
const cv = cvRef.current;
const ctx = ctxRef.current;
if (!field || !cv || !ctx) return;
const r = field.getBoundingClientRect();
wRef.current = Math.round(r.width);
hRef.current = Math.round(r.height);
cv.width = wRef.current;
cv.height = hRef.current;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.font = '600 10px -apple-system, "Segoe UI", sans-serif';
}
// 사전 적분 — 아이자와는 구 중심 근처, 토머스는 원점에서
function precompute(n: number): void {
const azX = new Float32Array(n), azY = new Float32Array(n), azZ = new Float32Array(n);
const thX = new Float32Array(n), thY = new Float32Array(n), thZ = new Float32Array(n);
let pa: Vec3 = [0.1, 0, 0], pt: Vec3 = [1.1, 1.2, -0.6];
for (let i = 0; i < 100; i++) { pa = rk4(pa, dAizawa); pt = rk4(pt, dThomas); }
for (let i = 0; i < n; i++) {
azX[i] = pa[0]; azY[i] = pa[1]; azZ[i] = pa[2]; pa = rk4(pa, dAizawa);
thX[i] = pt[0]; thY[i] = pt[1]; thZ[i] = pt[2]; pt = rk4(pt, dThomas);
}
azRef.current = { x: azX, y: azY, z: azZ };
thRef.current = { x: thX, y: thY, z: thZ };
}
// 한 판 그리기 — xz 투영 + y 깊이 음영. xz: x 가로, z 세로, y 를 사선 성분으로
function drawPane(
X: Float32Array, Y: Float32Array, Z: Float32Array,
cx: number, cy: number, s: number, col: RGB, head: RGB, n: number, fade: number, tilt: number,
ctx: CanvasRenderingContext2D
): void {
if (n < 2) return;
// 뒤네루게
ctx.strokeStyle = rgba(col, 0.12 * fade);
ctx.lineWidth = 3.0;
ctx.beginPath();
let hx = cx + (X[0] + Y[0] * tilt) * s, hy = cy - Z[0] * s;
ctx.moveTo(hx, hy);
for (let i = 1; i < n; i++) ctx.lineTo(cx + (X[i] + Y[i] * tilt) * s, cy - Z[i] * s);
ctx.stroke();
// 알맹이 — 걸음 진행에 따라 밝아지는 램프
const ramp = Math.max(0.10, 0.5 * (1 - n / steps) + 0.15);
ctx.strokeStyle = rgba(col, ramp * fade);
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(cx + (X[0] + Y[0] * tilt) * s, cy - Z[0] * s);
for (let i = 1; i < n; i++) ctx.lineTo(cx + (X[i] + Y[i] * tilt) * s, cy - Z[i] * s);
ctx.stroke();
// 머리
hx = cx + (X[n - 1] + Y[n - 1] * tilt) * s; hy = cy - Z[n - 1] * s;
ctx.fillStyle = rgba(head, 0.22 * fade);
ctx.beginPath(); ctx.arc(hx, hy, 5, 0, 6.2832); ctx.fill();
ctx.fillStyle = rgba(head, fade);
ctx.beginPath(); ctx.arc(hx, hy, 2, 0, 6.2832); ctx.fill();
}
function draw(phase: number, tSec: number): void {
const ctx = ctxRef.current;
const az = azRef.current;
const th = thRef.current;
const PANEL = panelRef.current;
const CA = caRef.current;
const CT = ctRef.current;
const HEAD = headRef.current;
const W = wRef.current, H = hRef.current;
if (!ctx || !az || !th || !PANEL || !CA || !CT || !HEAD) return;
ctx.fillStyle = rgba(PANEL, 1);
ctx.fillRect(0, 0, W, H);
const upto = Math.min(1, phase / REVEAL_END);
const n = Math.floor(upto * steps);
const fade = phase > REVEAL_END ? Math.max(0, 1 - (phase - REVEAL_END) / (1 - REVEAL_END)) : 1;
// 관찰축 회전 — 사선 성분 tilt 를 진동시킨다
const tilt = Math.sin(tSec / SPIN_L * 6.2832) * 0.5;
const mid = W / 2;
ctx.strokeStyle = rgba(CA, 0.14);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(mid, 0); ctx.lineTo(mid, H); ctx.stroke();
drawPane(az.x, az.y, az.z, mid * 0.52, H * 0.52, Math.min(W / 46, H / 42) * 0.6, CA, HEAD, n, fade, tilt, ctx);
drawPane(th.x, th.y, th.z, mid * 1.48, H * 0.5, Math.min(W / 46, H / 42) * 0.5, CT, HEAD, n, fade, tilt, ctx);
ctx.textAlign = "center";
ctx.fillStyle = rgba(CA, 0.9 * fade + 0.1);
ctx.fillText("AIZAWA", mid * 0.52, H - 6);
ctx.fillStyle = rgba(CT, 0.9 * fade + 0.1);
ctx.fillText("THOMAS", mid * 1.48, H - 6);
}
let t0 = -1;
function frame(now: number): void {
if (t0 < 0) t0 = now;
const tSec = (now - t0) / 1000;
draw((tSec % spinS) / spinS, tSec);
rafRef.current = requestAnimationFrame(frame);
}
useEffect(() => {
const root = rootRef.current;
const cv = cvRef.current;
if (!root || !cv) return;
caRef.current = tokRGB(root, "--lz-ca"); // 아이자와 — 라일락 화이트
ctRef.current = tokRGB(root, "--lz-ct"); // 토머스 — 민트 크림
headRef.current = tokRGB(root, "--lz-head");// 머리 — 크림(토큰)
panelRef.current = tokRGB(root, "--lz-panel");
ctxRef.current = cv.getContext("2d");
precompute(steps);
fit();
function onResize(): void {
fit();
if (reduced()) draw(0.62, 0.4);
}
window.addEventListener("resize", onResize);
if (reduced()) {
draw(0.62, 0.4);
setStill(true);
} else {
t0 = -1;
rafRef.current = requestAnimationFrame(frame);
}
return () => {
window.removeEventListener("resize", onResize);
cancelAnimationFrame(rafRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [steps, spinS]);
const rootCls = still ? `${styles.root} ${styles.isStill}` : styles.root;
return (
<main className={styles.stage}>
<div className={rootCls} ref={rootRef} style={vars}>
<p className={styles.hint}>
<span>{HINT}</span>
</p>
<div
className={styles.field}
ref={fieldRef}
role="application"
aria-label={FIELD_LABEL}
tabIndex={0}
>
<canvas className={styles.cv} ref={cvRef}></canvas>
</div>
<div className={styles.bar}>
<span className={styles.meta}>{META_LEFT}</span>
<span className={styles.metaDim}>{META_RIGHT}</span>
</div>
</div>
</main>
);
}05 アイザワ・トーマス — 球面と螺旋、別の方程式がくれた別の彫刻 — 計測 累積面積 0.768% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · ローレンツアトラクター 9種 — 蝶の形のカオス軌道を1歩ずつ数値積分して積む
このデモが属する記事
ローレンツアトラクター 9種 — 蝶の形のカオス軌道を1歩ずつ数値積分して積む3つの微分方程式を1歩ずつ積分すると、蝶の形をした軌道がひとりでに積み上がります。ライブラリなしのコピペで持ち帰れるカオス可視化です。ローレンツアトラクターとは、蝶の絵を持ってきた画像ではなく、速度式3行に点の位置を従わせた計算の結果です。 この記事のデモをすべて見る


