Style transfer

스타일 트랜스퍼

Keeping one image’s shape and layout while applying another image’s color, texture, and brushwork.

Also known as: Neural style transferStyle conditioning
···
html
<div class="wrap">
  <div class="top">
    <div class="mini"><canvas id="content"></canvas><span>내용</span></div>
    <div class="mini"><canvas id="swatch"></canvas><span id="sname">스타일: 수채</span></div>
  </div>
  <div class="result"><canvas id="result"></canvas><span>결과</span></div>
</div>
css
.wrap{width:min(340px,96%);height:92%;display:grid;grid-template-rows:auto 1fr;gap:8px}
.top{display:flex;gap:8px}
.mini{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px}
.mini canvas{width:100%;height:64px;border-radius:8px;border:1px solid var(--line)}
.mini span,.result span{font-size:9.5px;color:var(--muted);font-weight:700}
.result{display:flex;flex-direction:column;align-items:center;gap:3px;min-height:0}
.result canvas{width:100%;flex:1;border-radius:9px;border:1px solid var(--line);min-height:0}
js
const contentCv = document.getElementById('content'), swatchCv = document.getElementById('swatch'), resultCv = document.getElementById('result');
const sname = document.getElementById('sname');
function fitAll() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  [contentCv, swatchCv, resultCv].forEach((cv) => { const r = cv.getBoundingClientRect(); cv.width = Math.max(1, r.width) * dpr; cv.height = Math.max(1, r.height) * dpr; cv.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0); });
}
addEventListener('resize', fitAll);

function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }

function ridge(w, h, seed) {
  const r = mulberry32(seed);
  const pts = [];
  for (let i = 0; i <= 8; i++) { const x = w * i / 8; const y = h * 0.45 + Math.sin(i * 1.1 + seed) * h * 0.12 - r() * h * 0.08; pts.push([x, y]); }
  return pts;
}
const pts = { c: null };
function drawContent(w, h) {
  const ctx = contentCv.getContext('2d');
  ctx.clearRect(0, 0, w, h);
  pts.c = ridge(w, h, 3);
  ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--fg') || '#15151a';
  ctx.lineWidth = 1.6;
  ctx.beginPath(); ctx.moveTo(pts.c[0][0], pts.c[0][1]); pts.c.forEach(([x, y]) => ctx.lineTo(x, y)); ctx.stroke();
}
const styles = [
  { name: '수채', colors: ['#f2b5d4', '#f7dd72', '#8fd9c4'], pattern: 'blot' },
  { name: '잉크 그라데이션', colors: ['#2b2d63', '#7d3cff', '#00c2a8'], pattern: 'stripe' },
];
function paintSwatch(w, h, style) {
  const ctx = swatchCv.getContext('2d');
  ctx.clearRect(0, 0, w, h);
  const r = mulberry32(50);
  if (style.pattern === 'blot') {
    ctx.fillStyle = style.colors[0]; ctx.fillRect(0, 0, w, h);
    for (let i = 0; i < 10; i++) { ctx.fillStyle = style.colors[1 + (i % 2)]; ctx.globalAlpha = 0.6; ctx.beginPath(); ctx.arc(r() * w, r() * h, 6 + r() * 10, 0, Math.PI * 2); ctx.fill(); }
    ctx.globalAlpha = 1;
  } else {
    ctx.fillStyle = style.colors[0]; ctx.fillRect(0, 0, w, h);
    ctx.strokeStyle = style.colors[1]; ctx.lineWidth = 3;
    for (let x = -h; x < w; x += 7) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x + h, h); ctx.stroke(); }
  }
}
function paintResult(w, h, style) {
  const ctx = resultCv.getContext('2d');
  ctx.clearRect(0, 0, w, h);
  if (!pts.c) return;
  ctx.save();
  ctx.beginPath(); ctx.moveTo(0, h); pts.c.forEach(([x, y]) => ctx.lineTo(x, y)); ctx.lineTo(w, h); ctx.closePath(); ctx.clip();
  const r = mulberry32(77);
  if (style.pattern === 'blot') {
    ctx.fillStyle = style.colors[0]; ctx.fillRect(0, 0, w, h);
    for (let i = 0; i < 40; i++) { ctx.fillStyle = style.colors[1 + (i % 2)]; ctx.globalAlpha = 0.55; ctx.beginPath(); ctx.arc(r() * w, r() * h, 6 + r() * 14, 0, Math.PI * 2); ctx.fill(); }
    ctx.globalAlpha = 1;
  } else {
    ctx.fillStyle = style.colors[0]; ctx.fillRect(0, 0, w, h);
    ctx.strokeStyle = style.colors[1]; ctx.lineWidth = 4;
    for (let x = -h; x < w; x += 9) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x + h, h); ctx.stroke(); }
  }
  ctx.restore();
  ctx.strokeStyle = style.colors[2]; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(pts.c[0][0], pts.c[0][1]); pts.c.forEach(([x, y]) => ctx.lineTo(x, y)); ctx.stroke();
}
let si = 0;
function renderAll() {
  fitAll();
  const cr = contentCv.getBoundingClientRect(), sr = swatchCv.getBoundingClientRect(), rr = resultCv.getBoundingClientRect();
  drawContent(cr.width, cr.height);
  paintSwatch(sr.width, sr.height, styles[si]);
  paintResult(rr.width, rr.height, styles[si]);
  sname.textContent = '스타일: ' + styles[si].name;
}
renderAll();
setInterval(() => { si = (si + 1) % styles.length; renderAll(); }, 2200);

Classic neural style transfer separates "content" (shape and layout, represented by deep-layer activations) from "style" (color and texture, represented by correlations between feature maps), then recombines content from one image with style from another into a new one.

Diffusion-based tools today often implement the same idea through conditioning instead of optimization — feeding a style reference in like img2img, or attaching an adapter dedicated to style. The underlying split is the same: content decides "what is drawn," style decides "how it looks."

Content and style are not fully independent, so they can bleed into each other a little. Push style too hard and it can eat into the content's own edges; hold content too rigidly and style shows up as barely more than a tint.

The demo below is a procedural simulation, not real style separation and recombination — it keeps a fixed content outline and only repaints it with a different style swatch's palette and pattern.

When to use

Use it when the composition is settled and only the color and texture need to change. If the shape itself should change, img2img strength or ControlNet fits better.