इस पेज का अनुवाद मशीन द्वारा किया गया है और इसमें त्रुटियाँ हो सकती हैं। मूल अंग्रेज़ी पढ़ें

एलिमेंट

Shader

Shader एक ऐसा view है जिसका box एक GPU प्रोग्राम बनाता है: बहते हुए gradient, noise और grain, plasma, aurora, holographic foil, और tap का पीछा करती लहरें। आप एक WGSL फ़ंक्शन shade लिखते हैं, जो box के हर बिंदु का रंग देता है; प्लैटफ़ॉर्म उसे GPU पर स्क्रीन के resolution पर चलाता है, view के background के ऊपर और उसके children के नीचे। Animated values उसे native रूप से, फ़्रेम दर फ़्रेम चलाते हैं, ऐप के कोड में बिना कोई काम किए। TSX में <Shader> को nsh से बनाया जाता है, Rust में shader().source(program) से।

el-shader.tsx
el-shader.tsxLive
import { cleanup, createAnimatedValue, Easing, mount, Shader, signal, Text, View } from "nsh";
import { Animated, type CompositeAnimation } from "nsh";
import { Button, Switch } from "nsh/base";
import { button, buttonText, desktop, focusRing, hoverFill, Log, Panel, Segmented, size, Split, ui } from "./palette";

// A shader's colors come from uniforms; a tap starts a ripple, an animated
// uniform that runs natively; `paused` stops the shader's clock.
const SOURCE = /* wgsl */ `
struct Uniforms { a: vec4f, b: vec4f, c: vec4f, center: vec2f, wave: f32 }

fn shade(uv: vec2f) -> vec4f {
  let t = nsh.time * 0.4;
  let warp = vec2f(sin(uv.y * 4.0 + t * 1.3), cos(uv.x * 3.0 - t)) * 0.08;
  let q = uv + warp;
  let ab = mix(uniforms.a.rgb, uniforms.b.rgb, smoothstep(0.0, 1.0, q.x + 0.2 * sin(t)));
  var color = mix(ab, uniforms.c.rgb, smoothstep(0.2, 1.0, q.y + 0.15 * cos(t * 0.7)));
  let aspect = nsh.size / max(nsh.size.x, nsh.size.y);
  let d = distance(uv * aspect, uniforms.center * aspect);
  let ring = exp(-pow((d - uniforms.wave * 0.9) * 24.0, 2.0)) * (1.0 - uniforms.wave);
  color += vec3f(ring * 0.8);
  return vec4f(color, 1.0);
}`;

const PALETTES = {
  sunset: ["#ff6b6b", "#ffb86b", "#7b5cff"],
  lagoon: ["#00c2a8", "#2f80ed", "#0b3d91"],
  citrus: ["#f9f871", "#9be15d", "#ff9671"],
} as const;
type Palette = keyof typeof PALETTES;

function App() {
  const palette = signal<Palette>("sunset");
  const paused = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 5));
  const wave = createAnimatedValue(1);
  const cx = createAnimatedValue(0.5);
  const cy = createAnimatedValue(0.5);
  let box = { width: 1, height: 1 };
  let ripple: CompositeAnimation | undefined;
  cleanup(() => ripple?.stop());
  const tap = (x: number, y: number) => {
    ripple?.stop();
    cx.set(x);
    cy.set(y);
    wave.set(0);
    ripple = Animated.timing(wave, { toValue: 1, duration: 1600, easing: Easing.out(Easing.quad) });
    ripple.start();
    log(`Ripple at ${Math.round(x * 100)}%, ${Math.round(y * 100)}%`);
  };
  const colors = () => PALETTES[palette()];
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>shader</Text>
      <Text style={{ color: ui().muted }}>{desktop ? "Click the shader to start a ripple" : "Tap the shader to start a ripple"}</Text>

      <Split
        gap={14}
        width={760}
        inspector={
          <>
            <Segmented
              label="colors"
              options={["sunset", "lagoon", "citrus"] as const}
              value={palette()}
              onChange={(next) => {
                palette.set(next);
                log(`Colors: ${next}`);
              }}
            />
            <Panel title="paused · ripple">
              <Switch
                checked={paused()}
                onChange={({ checked }) => {
                  paused.set(checked);
                  log(checked ? "Paused: the clock stands still" : "Running");
                }}
                class={`-mx-2 flex-row-reverse items-center justify-between gap-3 rounded-lg px-2 ${desktop ? "py-1" : "py-2"} active:opacity-70`}
                hoverStyle={hoverFill()}
                focusStyle={focusRing()}
                trackClass={`${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`}
                trackStyle={paused() ? ui().accentFill : ui().inset}
                thumbClass={desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5"}
                thumbStyle={paused() ? { backgroundColor: ui().onAccent } : ui().raised}
              >
                <Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Paused</Text>
              </Switch>
              <Button onPress={() => tap(0.5, 0.5)} {...button("secondary", { style: desktop ? { alignSelf: "flex-start" } : {} })}>
                <Text style={buttonText()}>Ripple</Text>
              </Button>
            </Panel>
            <Log title="events" lines={events()} minHeight={92} />
          </>
        }
      >
        <Shader
          testID="shader"
          source={SOURCE}
          paused={paused()}
          uniforms={{ a: colors()[0], b: colors()[1], c: colors()[2], center: [cx, cy], wave }}
          style={{ height: desktop ? 420 : 280, borderRadius: 16, padding: 18, justifyContent: "flex-end" }}
          onLayout={({ bounds }) => (box = bounds.size)}
          onStartShouldSetResponder={() => true}
          onResponderGrant={(e) => tap(e.locationX / box.width, e.locationY / box.height)}
        >
          <Text style={{ color: "#ffffff", fontSize: 15, fontWeight: "600" }}>Drawn by a WGSL shader, under its children</Text>
        </Shader>
      </Split>
    </View>
  );
}

mount(App);
use nsh_core::{prelude::*, shader::Shader};

const GLOW: &str = r#"
struct Uniforms { tint: vec4f }

fn shade(uv: vec2f) -> vec4f {
  let d = distance(uv, vec2f(0.5));
  let pulse = 0.5 + 0.5 * sin(nsh.time * 2.0);
  let glow = smoothstep(0.5, 0.0, d) * (0.6 + 0.4 * pulse);
  return vec4f(uniforms.tint.rgb * glow, 1.0);
}
"#;

fn glow() -> impl IntoElement {
    let program = Shader::compile(GLOW).expect("a valid shader");
    shader()
        .source(program)
        .uniform("tint", Hsla::from(rgb(0x38bdf8)))
        .h(px(200.0))
        .rounded(px(14.0))
}

#शेडर

Shader का सोर्स WGSL में होता है, जो WebGPU की shading भाषा है। यह एक फ़ंक्शन परिभाषित करता है:

fn shade(uv: vec2f) -> vec4f
  • uv view के box का बिंदु है, ऊपर-बाएँ कोने पर (0, 0) से नीचे-दाएँ कोने पर (1, 1) तक; y नीचे की ओर बढ़ता है, जैसे CSS में।
  • नतीजा उस बिंदु का रंग है: red, green, blue और alpha 0..1 में, sRGB में straight alpha के साथ, जैसे CSS रंग होते हैं। 0..1 से बाहर के मान clamp कर दिए जाते हैं।

shade के साथ सोर्स में जितने helper फ़ंक्शन, constants और var<private> वेरिएबल चाहिए, उतने हो सकते हैं। NSH entry points देता है और shader के inputs दो जगहों से पढ़ता है।

#Built-ins

Built-inटाइपमान
nsh.sizevec2fview का size, points में।
nsh.timef32shader के प्रकट होने के बाद के सेकंड। जो shader इसे पढ़ता है वह animate होता है: जब तक वह स्क्रीन पर है, हर फ़्रेम draw करता है।
nsh.scalef32जिस फ़्रेम को draw किया जा रहा है उसके प्रति point pixels।

uv * nsh.size points में बिंदु है; nsh.size.x / nsh.size.y aspect ratio है, जो गैर-वर्गाकार box में वृत्तों को गोल रखता है।

#Uniforms

ऐप जो मान सेट करता है वे Uniforms नाम के struct में जाते हैं, जिसे shader uniforms के रूप में पढ़ता है:

struct Uniforms {
  colors: array<vec4f, 4>,
  speed: f32,
  center: vec2f,
}

fn shade(uv: vec2f) -> vec4f {
  let t = nsh.time * uniforms.speed;
  // …
}

uniforms prop इसके members को नाम से सेट करता है। हर टाइप ये मान लेता है:

WGSL टाइपमान
f32, i32, u32एक number (i32 और u32 के लिए शून्य की ओर पूर्णांकित), या animated value
vec2f[x, y]: दो numbers या animated values
vec3fएक रंग (उसके red, green और blue), animated रंग, या तीन numbers
vec4falpha के साथ एक रंग, animated रंग, या चार numbers
array<vec4f, N>अधिकतम N रंगों की सूची (या हर एक में चार numbers): palette, gradient के stops

रंग कोई भी CSS रंग हो सकते हैं ("#38bdf8", "rgb(56 189 248)", "hsl(199 93% 60%)") और shader तक 0..1 में sRGB components के रूप में पहुँचते हैं। ऐप जिन members को छोड़ देता है वे शून्य होते हैं। uniforms को फिर से सेट करने पर shader नए मानों के साथ दोबारा draw होता है।

const palettes = { Sunset: ["#ff6b6b", "#ffb86b", "#7b5cff", "#ff4fa3"], Lagoon: ["#00c2a8", "#2f80ed", "#a0f0ff", "#0b3d91"] };
const palette = signal<keyof typeof palettes>("Sunset");

<Shader source={MESH} uniforms={{ colors: palettes[palette()], speed: 0.5 }} style={{ height: 200 }} />;

#Animated uniforms

Uniform एक native animated value हो सकता है, या उससे बना कोई node (interpolate, arithmetic)। प्लैटफ़ॉर्म हर उस फ़्रेम पर, जिसमें उसका मान बदलता है, उसे shader में लिखता है और दोबारा draw करता है, UI थ्रेड पर: ऐप का कोड कितना भी व्यस्त हो, animation पूरे frame rate पर चलता है। Numbers f32, i32 और u32 members तथा vec2f के components को animate करते हैं; रंगों की interpolation vec3f या vec4f को animate करती है।

use nsh_core::{prelude::*, shader::Shader};

fn ripple(program: Shader) -> impl IntoElement {
    let wave = animated_value(0.0);
    wave.timing(1.0, 2200.0, Easing::Linear);
    shader()
        .source(program)
        .uniform("wave", &wave)
        .uniform("center", [0.5_f32, 0.5])
        .h(px(200.0))
}

#Pausing

paused shader की घड़ी रोक देता है: nsh.time अपना मान बनाए रखता है और shader अपनी तस्वीर थामे रखता है। इसे वापस false पर सेट करने से घड़ी वहीं से चलती है जहाँ रुकी थी। Uniforms और size में बदलाव फिर भी रुके हुए shader को draw करते हैं।

const paused = signal(false);

<Shader source={AURORA} paused={paused()} style={{ height: 200 }} onPress={() => paused.set(!paused())} />;

जो shader nsh.time नहीं पढ़ता, वह अपना size या uniforms बदलने पर draw होता है, और बीच में अपनी तस्वीर थामे रखता है।

#Children और clipping

Shader एक container है: यह view का हर prop लेता है (styles, उसके children का padding और layout, press और gesture props, onLayout, accessibility, ref), और उसके children shader के ऊपर draw होते हैं। Shader view के box को उसी तरह भरता है जैसे background भरता है, उसके गोल कोनों (borderRadius) के अंदर, उसके backgroundColor के ऊपर।

<Shader source={HOLO} uniforms={{ tilt }} style={{ height: 200, borderRadius: 18, padding: 20, justifyContent: "space-between" }}>
  <Text style={{ color: "#ffffff", fontSize: 13, fontWeight: "600" }}>NATIVE SHELL</Text>
  <Text style={{ color: "#ffffff", fontSize: 26, fontWeight: "700" }}>4242 0000 1717</Text>
</Shader>

Shader को size उसके styles से दें (width / height, flex, aspect ratio) या उसके children को तय करने दें।

#Effects, transitions और screen

Shader वह भी draw कर सकता है जो दूसरे views draw करते हैं। तब उसका WGSL चार फ़ंक्शनों से तस्वीरें पढ़ता है, जिनमें से हर एक अपने box का एक बिंदु uv लेता है और alpha के साथ straight sRGB रंग लौटाता है, तस्वीर के बाहर पारदर्शी:

Functionक्या पढ़ता है
sample_content(uv)किसी effect के children, या screen effect के लिए पूरी surface
sample_backdrop(uv)view के पीछे क्या है (कोई भी shader इसे पढ़ सकता है)
sample_from(uv)transition का वह पक्ष जिसे वह छोड़ रहा है
sample_to(uv)transition का वह पक्ष जिस पर वह आ रहा है

nsh.progress effect का progress है या यह कि transition कितना आगे बढ़ा है (0 से 1), और nsh.overflow यह कि shader box के बाहर कितनी दूर तक draw करता है, points में।

ShaderEffect अपने children को shader से होकर draw करता है: वे अपने presses, focus और accessibility बनाए रखते हैं, और जो दिखता है वह shader का नतीजा है। progress एक number या animated value है, जो effect को native रूप से चलाती है। maxSampleOffset shader को box के बाहर उतने points तक draw करने देता है (glow, blur का किनारा); तब उसका uv 0 से नीचे और 1 से ऊपर जाता है। source के बिना children जैसे हैं वैसे ही draw होते हैं।

import { createAnimatedValue, ShaderEffect, Text } from "nsh";

const WIPE_OUT = `
fn shade(uv: vec2f) -> vec4f {
  let c = sample_content(uv);
  let edge = smoothstep(nsh.progress - 0.05, nsh.progress + 0.05, uv.x);
  return vec4f(c.rgb, c.a * edge);
}`;

const gone = createAnimatedValue(0);

<ShaderEffect source={WIPE_OUT} progress={gone} style={{ padding: 16 }}>
  <Text>Wiped away as `gone` goes to 1</Text>
</ShaderEffect>

ShaderTransition अपने children को shader से बदलता है जब value (string, number या boolean) बदलता है, पहले मान पर नहीं: sample_from उस चीज़ की तस्वीर है जो उन्होंने पहले draw की थी, sample_to वह जो वे अब draw करते हैं, और nsh.progress duration milliseconds (300) में 0 से 1 तक जाता है। source के बिना यह fallback चलाता है: "fade" (cross-fade, डिफ़ॉल्ट) या "none"।

<ShaderTransition source={WIPE} value={card()} duration={450}>
  <Card name={card()} />
</ShaderTransition>

ScreenEffect और ScreenTransition पूरी surface (उसके pages, dialogs और overlays) के लिए यही करते हैं। वे जगह नहीं घेरते और उनके children नहीं होते; उन्हें ऐप में कहीं भी रखें। source के साथ mount हुआ आख़िरी screen effect draw होता है।

Pages और dialogs transitionShader के साथ shader से होकर आते-जाते हैं: { source, uniforms, duration } किसी NavPage या Dialog पर (Rust: .transition_shader(TransitionShader::new(program)) nav_page() या dialog() पर)। जब page push या pop होता है, या dialog दिखाया या बंद किया जाता है, तो निचला पक्ष (उसके नीचे का page, dialog के बिना ऐप) sample_from होता है और ऊपरी पक्ष sample_to; nsh.progress page या dialog के आने पर 0 से 1 तक और जाने पर 1 से 0 तक जाता है, इसलिए एक ही shader दोनों दिशाओं में draw करता है।

use std::time::Duration;

use nsh_base::nav_page;
use nsh_core::{prelude::*, shader::{Shader, TransitionShader}};

fn details() -> impl IntoElement {
    let ripple = Shader::compile(RIPPLE).expect("a valid shader");
    nav_page()
        .title("Details")
        .transition_shader(TransitionShader::new(ripple).duration(Duration::from_millis(500)))
        .child(text("…"))
}

Shader page या dialog के animation की जगह लेता है, चाहे वह कुछ भी हो ("none" भी)। animation उस डिवाइस पर चलता है जिसमें GPU नहीं है, जब उपयोगकर्ता कम motion माँगता है (Reduce Motion, Remove animations), और ऐसे सोर्स के लिए जो compile नहीं होता; "none" के साथ तब बदलाव तुरंत दिखता है। जो push, pop, show या close किसी दूसरे के चलते समय शुरू होता है, वह पहले उसे पूरा करता है: उसका page या dialog आ या चला जाता है, उसका onShow या onDismiss fire होता है, और नया बदलाव वहीं से शुरू होता है।

प्लैटफ़ॉर्म तस्वीरें तब लेते हैं जब जो वे दिखाते हैं वह बदलता है, प्रति display frame अधिकतम एक बार। तस्वीर प्लैटफ़ॉर्म के अपने renderer से draw होती है, इसलिए जिसे वह तस्वीर में draw नहीं कर सकता (video, web page, किसी दूसरे ऐप का view) वह उसमें पारदर्शी दिखता है।

#Props

Shader view के props लेता है, साथ में:

PropRustटाइपविवरण
source.source(program)stringfn shade(uv: vec2f) -> vec4f को परिभाषित करने वाला WGSL सोर्स। Rust इसे Shader::compile(source) से compile करता है।
uniforms.uniform(name, value)Record<string, ShaderUniform>struct Uniforms के members के मान, नाम से: numbers, रंग, animated values और उनकी सूचियाँ।
paused.paused(bool)booleannsh.time की घड़ी रोकता है। डिफ़ॉल्ट false।

ShaderEffect और ScreenEffect ये लेते हैं (source वैकल्पिक), साथ में:

PropRustटाइपविवरण
progress.progress(n), .animated_progress(&value)number | AnimatedNodensh.progress। डिफ़ॉल्ट 0।
maxSampleOffset.max_sample_offset(px(n))numberbox के बाहर कितनी दूर तक, points में, shader draw करता है और उसकी तस्वीरें पहुँचती हैं (ShaderEffect और ShaderTransition)। डिफ़ॉल्ट 0।

ShaderTransition और ScreenTransition source, uniforms और paused लेते हैं, साथ में:

PropRustटाइपविवरण
value.value(key)string | number | booleanइसके बदलने पर transition चलता है (पहले मान पर नहीं)।
duration.duration(Duration)numberकितनी देर चलता है, milliseconds में। डिफ़ॉल्ट 300।
fallback.fallback(TransitionFallback)"fade" | "none"source के बिना क्या चलता है। डिफ़ॉल्ट "fade"।

NavPage और Dialog लेते हैं:

PropRustटाइपविवरण
transitionShader.transition_shader(TransitionShader::new(program)), .uniform(name, value), .duration(Duration){ source: string; uniforms?: Record<string, ShaderUniform>; duration?: number }वह shader जिससे होकर page या dialog अपने animation की जगह आता-जाता है। duration milliseconds में, डिफ़ॉल्ट 350।

#Errors

NSH हर shader को तब जाँचता है जब उसका सोर्स सेट होता है, GPU तक पहुँचने से पहले। जो shader compile नहीं होता वह सोर्स में line और column तथा क्या गलत है, इसके साथ एक warning log करता है, उदाहरण के लिए:

the shader of node 12: 2:1: `shade` takes the point's `uv: vec2f` and returns its color, a `vec4f`

तब view अपना background और children दिखाता है। जो uniform struct में नहीं है, या ऐसा मान जिसे उसका टाइप नहीं लेता, उस uniform का नाम बताने वाली warning log करता है, और बाकी uniforms लागू होते हैं।

हर सोर्स एक बार compile होता है: जो views एक सोर्स साझा करते हैं, और दोबारा सेट किया गया सोर्स, compile किए हुए shader का उपयोग करते हैं।

#Rules और limits

  • shade का signature ठीक fn shade(uv: vec2f) -> vec4f है। सोर्स अपने functions, constants, structs और var<private> वेरिएबल खुद declare करता है; NSH entry points और bindings देता है।
  • सोर्स अधिकतम 64 KiB का हो सकता है।
  • struct Uniforms अधिकतम 4096 bytes लेता है, जिसमें members ऊपर की तालिका के टाइप के होते हैं।
  • एक फ़्रेम की हर तरफ़ अधिकतम 4096 pixels होती हैं: बड़ा view अपना shader कम scale पर draw करता है।

Noise के लिए, उस integer cell को hash करें जिसमें बिंदु है (bitcast<vec2u>(vec2i(floor(p))) को multiplies और xor-shifts से मिलाकर), fract(sin(…) * 43758.5) की बजाय: integer hashes एक cell के लिए हर उस जगह वही मान देते हैं जहाँ उसकी गणना होती है, इसलिए value noise हर GPU पर seamless रहता है। Gallery के shaders में एक उदाहरण है।

#Platforms

iOS, Android, macOS, Windows और Linux पर, और GPUI ऐप्स में, NSH shaders को GPU पर wgpu से render करता है (Apple प्लैटफ़ॉर्म पर Metal, अन्यत्र Vulkan, Direct3D 12 या OpenGL), स्क्रीन के scale पर, और हर फ़्रेम को view के children के नीचे दिखाता है। Rendering UI के साथ-साथ चलती है: फ़्रेम draw होने के एक या दो display frame बाद दिखता है, और GPU के काम करते समय UI responsive रहता है। Web पर, NSH shader को GLSL में translate करता है और WebGL 2 से draw करता है।

Shaders प्रति display frame अधिकतम एक बार draw होते हैं, और केवल तब जब जो वे दिखाते हैं वह बदलता है: fixed uniforms वाला shader एक बार draw होता है, जो shader nsh.time पढ़ता है वह हर फ़्रेम, और animated uniforms वाला shader तब तक जब तक वे चलते हैं।

#Rust

shader() (nsh_core::element से) एक shader view बनाता है; .source(program) उसे compile किया हुआ Shader देता है (nsh_core::shader::Shader::compile(source), जो ऊपर जैसी ही errors लौटाता है)। प्रोग्राम को एक बार compile करें और साझा करें: उसे clone करना सस्ता है। प्रोग्राम bound हो सकता है, .source(move || …), प्रोग्राम बदलने के लिए।

Methodविवरण
.uniform(name, value)struct Uniforms का एक member सेट करता है: number, Color या Hsla, [f32; 2] / [f32; 3] / [f32; 4], या Vec<UniformValue>। Bound मान (move || …) बदलने पर उसे फिर सेट करता है; animated मान (&wave: number, या vec3f / vec4f के लिए रंग) उसे हर फ़्रेम पर native रूप से चलाता है।
.paused(bool)nsh.time की घड़ी रोकता है; bindable।

Children .child(..) से जोड़े जाते हैं, styles सामान्य typed methods से। shader_effect(), shader_transition(), screen_effect() और screen_transition() बाकी elements बनाते हैं, ऊपर की तालिकाओं के methods के साथ।