एलिमेंट
Image
Image ऐप के assets या किसी URL से एक चित्र दिखाता है। चित्र मुख्य थ्रेड के बाहर डाउनलोड और डिकोड किए जाते हैं, उन्हें उस आकार में डाउनसैंपल किया जाता है जिसमें व्यू उन्हें दिखाता है, और मेमोरी में कैश किया जाता है। एनिमेटेड GIF, WebP और APNG चित्र चलते हैं। Rust में इसे image(src) से बनाया जाता है, TSX में nsh से <Image> द्वारा।
import { Image, mount, signal, Text, View } from "nsh";
import { windowSize } from "nsh";
import { button, buttonText, focusRing, ui } from "./palette";
// The pictures are SVG `data:` URIs, so the demo needs no network. An app
// would import a file next to it (`import hills from "./hills.png"`) or pass
// a URL.
const svg = (body: string, width: number, height: number) =>
`data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`)}`;
const landscape = svg(
`<defs><linearGradient id="s" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#7ea8ff"/><stop offset="1" stop-color="#ffd59e"/></linearGradient></defs>
<rect width="1200" height="800" fill="url(#s)"/>
<circle cx="880" cy="250" r="90" fill="#fff4c2"/>
<path d="M0 560 Q 250 380 520 520 T 1200 470 V800 H0Z" fill="#6aa86b"/>
<path d="M0 650 Q 330 520 700 640 T 1200 610 V800 H0Z" fill="#3f7d4c"/>
<text x="40" y="770" font-family="system-ui" font-size="40" fill="#ffffff">1200 × 800</text>`,
1200,
800,
);
const portrait = svg(
`<defs><linearGradient id="n" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1b1f3a"/><stop offset="1" stop-color="#b79cff"/></linearGradient></defs>
<rect width="600" height="900" fill="url(#n)"/>
<circle cx="300" cy="260" r="120" fill="#f4f1ff"/>
<circle cx="350" cy="230" r="110" fill="#2a2f55"/>
<path d="M0 700 L 180 520 L 330 660 L 450 560 L 600 700 V900 H0Z" fill="#141629"/>
<text x="30" y="870" font-family="system-ui" font-size="36" fill="#ffffff">600 × 900</text>`,
600,
900,
);
// Shown until the picture loads, and in its place when it fails.
const placeholder = svg(
`<rect width="120" height="80" fill="#8d9198" opacity="0.25"/><path d="M20 62 L48 34 L68 52 L80 42 L100 62Z" fill="#8d9198"/><circle cx="86" cy="24" r="8" fill="#8d9198"/>`,
120,
80,
);
// An icon drawn in one color: `tintColor` recolors it.
const star = svg(`<path d="M12 2l3 6.5 7 .8-5.2 4.8 1.5 7L12 17.6 5.7 21.1l1.5-7L2 9.3l7-.8z" fill="#000"/>`, 24, 24);
const sources = { landscape, portrait, missing: "asset:/missing.png" } as const;
type Source = keyof typeof sources;
const modes = ["cover", "contain", "fill", "none"] as const;
type Mode = (typeof modes)[number];
function Segments<T extends string>(props: { options: readonly T[]; value: T; onChange: (v: T) => void }) {
return (
<View style={{ ...ui().inset, flexDirection: "row", padding: 3, borderRadius: 10 }}>
<Each of={props.options}>
{(option) => {
const on = () => props.value === option();
return (
<View
onPress={() => props.onChange(option())}
style={{
flex: 1,
alignItems: "center",
paddingVertical: 6,
borderRadius: 8,
backgroundColor: on() ? ui().raised.backgroundColor : "transparent",
backgroundImage: on() ? ui().raised.backgroundImage : "none",
boxShadow: on() ? ui().raised.boxShadow : "none",
}}
hoverStyle={{ opacity: 0.88 }}
focusStyle={focusRing()}
activeStyle={{ opacity: 0.7 }}
>
<Text style={{ fontWeight: "600", color: on() ? ui().text : ui().muted }}>{option()}</Text>
</View>
);
}}
</Each>
</View>
);
}
function App() {
const source = signal<Source>("landscape");
const mode = signal<Mode>("cover");
const blur = signal(false);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 4));
const wide = () => windowSize().width >= 700;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>image</Text>
<View style={{ flex: 1, flexDirection: wide() ? "row" : "column", gap: 16 }}>
<Image
src={sources[source()]}
placeholder={placeholder}
blurRadius={blur() ? 12 : 0}
fadeDuration={250}
onLoadStart={() => log("onLoadStart")}
onLoad={({ size }) => log(`onLoad ${Math.round(size.width)} × ${Math.round(size.height)}`)}
onError={(e) => log(`onError ${e.message}`)}
onLoadEnd={() => log("onLoadEnd")}
style={{ ...ui().inset, flex: 1, minHeight: 180, borderRadius: 14, objectFit: mode() }}
/>
<View style={{ gap: 12, width: wide() ? 320 : undefined }}>
<Text style={{ color: ui().muted }}>src</Text>
<Segments options={Object.keys(sources) as Source[]} value={source()} onChange={(s) => source.set(s)} />
<Text style={{ color: ui().muted }}>objectFit</Text>
<Segments options={modes} value={mode()} onChange={(m) => mode.set(m)} />
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<View onPress={() => blur.set(!blur())} {...button("secondary", { on: blur })}>
<Text style={buttonText("secondary", { on: blur })}>blurRadius {blur() ? 12 : 0}</Text>
</View>
<Text style={{ color: ui().muted }}>tintColor</Text>
<Image src={star} style={{ width: 24, height: 24, tintColor: ui().accent }} />
<Image src={star} style={{ width: 24, height: 24, tintColor: ui().info }} />
</View>
<View style={{ ...ui().card, borderRadius: 12, padding: 12, gap: 4 }}>
<Text style={{ color: ui().muted, fontSize: 12 }}>Events, newest first</Text>
<Each of={events()}>
{(line) => <Text style={{ color: ui().text, fontFamily: "monospace", fontSize: 13 }}>{line()}</Text>}
</Each>
</View>
</View>
</View>
</View>
);
}
mount(App);use nsh_core::prelude::*;
// assets/logo.png in the app's crate.
const LOGO: ImageAsset = asset!("logo.png");
view()
.flex_row()
.gap_3()
.child(image(LOGO))
.child(
image("https://example.com/avatar.jpg")
.placeholder(LOGO)
.object_fit(ResizeMode::Cover)
.size(px(64.0))
.rounded_full(),
)import { Image, View } from "nsh";
import logo from "./assets/logo.png";
<View style={{ flexDirection: "row", gap: 12 }}>
<Image src={logo} />
<Image
src="https://example.com/avatar.jpg"
placeholder={logo}
style={{ width: 64, height: 64, borderRadius: 32 }}
/>
</View>;एक asset अपना आकार जानता है: चौड़ाई या ऊँचाई न देने पर चित्र वही आकार लेता है, और इनमें से किसी एक के देने पर दूसरी asset के aspect ratio के अनुसार तय हो जाती है। URL का आकार लोड होने के बाद ही पता चलता है, इसलिए रिमोट चित्रों को चौड़ाई और ऊँचाई दें (या इनमें से किसी एक के साथ aspect ratio)।
#Sources
source (src और placeholder; Rust में image(..) और .placeholder(..)) ये मान लेता है:
| मान | उदाहरण | विवरण |
|---|---|---|
| एक asset | Rust asset!("logo.png"), TSX import logo from "./logo.png" | एक ImageAsset { url, size, scales? }, जिसका आकार points में है: एक @2x फ़ाइल अपने पिक्सेल का आधा गिनी जाती है। logo.png के साथ logo@2x.png / logo@3x.png होने पर स्क्रीन के pixel ratio वाला वेरिएंट लोड होता है (pixel densities)। .png, .jpg, .jpeg, .gif, .webp और .heic इम्पोर्ट किए जा सकते हैं, और .svg भी, जिसका आकार उसके width और height या viewBox से तय होता है (SVG images)। |
| एक URL | "https://example.com/a.jpg" | डाउनलोड और कैश किया जाता है। एक ही URL के डाउनलोड व्यूज़ के बीच साझा होते हैं। |
| एक asset path | "asset:/images/a.png" | ऐप के assets की एक फ़ाइल। |
एक data: URI | "data:image/png;base64,iVBOR…" | URI में मौजूद bytes, base64 या percent-encoded (data:image/svg+xml,%3Csvg…)। |
एक blob: URL | URL.createObjectURL(blob) | किसी Blob या File (TSX) के bytes। चित्र उन्हें अपने पास रखता है: URL को revoke करने पर भी चित्र पूरा दिखता रहता है। |
जिन bytes से चित्र बनता है यदि वे ऐप के पास पहले से हैं, तो वह बिना किसी request के दिखता है। एक चैट चुनी गई फ़ोटो को अपलोड होने के दौरान उसके blob से दिखाती है, फिर src को फ़ोटो के URL पर बदल देती है:
// Bytes in a `data:` URI (an image the app made or received inline).
image(format!("data:image/png;base64,{encoded}")).size(px(64.0))import { Image, signal } from "nsh";
const preview = URL.createObjectURL(photo); // the picked `File`
const src = signal(preview);
upload(photo).then((url) => {
src.set(url);
URL.revokeObjectURL(preview);
});
<Image src={src()} style={{ width: 240, height: 180 }} />;समान bytes एक ही चित्र हैं: वही तस्वीर data: URI के रूप में और blob के रूप में एक बार ही डिकोड होती है और कैश साझा करती है।
डेवलपमेंट के दौरान assets dev server से परोसे जाते हैं; nsh build और nsh package उन्हें ऐप में कॉपी करते हैं (एक Rust crate अपनी assets/ डायरेक्टरी को registry.assets(assets!()) के साथ भेजता है)। देखें images and assets।
#Props
common props के अतिरिक्त (style का type ImageStyle है):
| Prop | Rust | Type | Default | विवरण |
|---|---|---|---|---|
src | image(src) | string | ImageAsset | चित्र। | |
placeholder | .placeholder(src) | string | ImageAsset | src के लोड होने तक दिखाया जाता है; बेहतर है कि यह एक लोकल asset हो। | |
blurRadius | .blur_radius(px(..)) | number | चित्र को points में दी गई इस त्रिज्या से धुंधला करता है (डिकोड करते समय एक बार लागू होता है, मुख्य थ्रेड के बाहर)। | |
fadeDuration | .fade_duration(ms) | number | iOS 0, Android 300 | लोड हुआ चित्र कितनी देर में fade in होता है, मिलीसेकंड में। |
tintColor (style) | .tint_color(..) | ColorValue | एक template चित्र (एक रंग में बना आइकन) का रंग बदलता है। | |
objectFit (style) | .object_fit(ResizeMode::Contain) | "cover" | "contain" | "fill" | "none" | "scale-down" | "cover" | चित्र अपने बॉक्स में कैसे बैठता है, CSS के object-fit की तरह (object-* क्लासेस): "cover" उसे भरता है और क्रॉप करता है, "contain" उसके अंदर फ़िट करता है, "fill" खींचता है, "none" उसे उसके प्राकृतिक आकार में दिखाता है, "scale-down" वही, पर जहाँ वह बड़ा है वहाँ सिकोड़कर फ़िट करता है। |
objectPosition (style) | .object_position(..), .object_top(), … | string | "center" | objectFit द्वारा आकार तय होने के बाद चित्र अपने बॉक्स में कहाँ रहता है, CSS के object-position की तरह ("top", "25% 75%", "right 10px bottom 20%"); पूरे बॉक्स को ढकने वाला चित्र इसके आसपास से क्रॉप होता है। |
onLoadStart | .on_load_start(..) | () => void | लोडिंग शुरू हुई। | |
onProgress | .on_progress(..) | (e: { loaded: number; total: number }) => void | bytes में डाउनलोड की प्रगति (अज्ञात होने पर total 0 होता है), अधिकतम हर 100 ms पर। | |
onLoad | .on_load(..) | (e: { size: PixelSize }) => void | लोड हो गया; स्रोत चित्र का आकार पिक्सेल में (डिकोड या डाउनसैंपल किया हुआ नहीं; SVG का आकार points में, वेब पर वैसा जैसा ब्राउज़र मापता है); variants वाले चित्र के लिए, लोड हुए variant का आकार। | |
onError | .on_error(..) | (e: { message: string; code?: string }) => void | लोडिंग विफल हुई; ऐप की capabilities जिस URL की अनुमति नहीं देतीं, उसके लिए code "ERR_ACCESS_DENIED" होता है। | |
onLoadEnd | .on_load_end(..) | () => void | onLoad या onError के बाद। |
ImageStyle व्यू styles में tintColor जोड़ता है, जो एक template चित्र (एक रंग में बना आइकन) का रंग बदलता है, और objectPosition, जो चित्र को उसके बॉक्स में रखता है (Image position)।
image(ICON).size(px(24.0)).tint_color(rgb(0x0969da))<Image src={icon} style={{ width: 24, height: 24, tintColor: "#0969da" }} />#Events
| Event | Rust | Payload | कब |
|---|---|---|---|
onLoadStart | .on_load_start | कोई नहीं | लोडिंग शुरू हुई। |
onProgress | .on_progress | { loaded, total } (ImageProgress) | bytes आए (अधिकतम हर 100 ms पर)। |
onLoad | .on_load | { size } (Size<DevicePixels>) | चित्र दिखाया गया; उसका स्रोत आकार पिक्सेल में (SVG का points में, वेब पर वैसा जैसा ब्राउज़र मापता है)। |
onError | .on_error | { message, code? } (&str, संदेश) | लोडिंग या डिकोडिंग विफल हुई, या होस्ट ऐप की capabilities के बाहर है (code "ERR_ACCESS_DENIED")। |
onLoadEnd | .on_load_end | कोई नहीं | onLoad या onError के बाद। |
let state = signal(SharedString::from("loading"));
image(url)
.w_full()
.aspect_ratio(16.0 / 9.0)
.on_load(move |size: &Size<DevicePixels>| state.set(format!("{}×{}", size.width.0, size.height.0).into()))
.on_error(move |message: &str| state.set(message.to_string().into()))const state = signal("loading");
<Image
src={url}
style={{ width: "100%", aspectRatio: 16 / 9 }}
onLoad={({ size }) => state.set(`${size.width}×${size.height} px`)}
onError={(e) => state.set(e.message)}
/>;pixel-density variants वाला चित्र अपनी फ़ाइलों में से एक लोड करता है, और onLoad उसी फ़ाइल के पिक्सेल बताता है: 3× स्क्रीन पर logo@3x.png के लिए 192×192, जो 64×64 points के logo.png के लिए है। जब pixel ratio बदलता है और चित्र कोई दूसरा variant लोड करता है, तो वह उस लोड की भी सूचना देता है: onLoadStart, फिर नई फ़ाइल के आकार के साथ onLoad (या onError), फिर onLoadEnd।
किसी चित्र का आकार जानने या उसे दिखाने से पहले लोड करने के लिए, TSX में Image.getSize(src) और Image.prefetch(src) को कॉल किया जाता है (वही Image जो एलिमेंट को रेंडर करता है, इन्हें भी रखता है; देखें Image API), और Rust में image_size(src) और prefetch_image(src) को, किसी task में await करके। variants वाला इम्पोर्ट किया हुआ चित्र देने पर Image.getSize और Image.prefetch वही variant लोड करते हैं जो यह स्क्रीन दिखाती है, और getSize इम्पोर्ट की गई फ़ाइल के आकार के साथ resolve होता है (logo.png के लिए 64×64)।
#Ref methods
इसके अपने कोई नहीं।
#Platform notes
- कैश key में डिकोड आकार और blur त्रिज्या शामिल हैं: एक ही URL को दो आकारों में दिखाने पर वह दो बार डिकोड होता है पर एक बार डाउनलोड होता है।
- iOS एनिमेटेड चित्रों को keyframe animation के रूप में चलाता है (अधिकतम 600 फ़्रेम); tint किया हुआ एनिमेटेड चित्र अपना पहला फ़्रेम दिखाता है।
- Android एनिमेटेड चित्रों को API 28 (
AnimatedImageDrawable) से चलाता है, बिना कैश और बिना blur के। - Android पर borders चित्र के ऊपर बनाए जाते हैं, जैसा iOS पर।