Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Dokumentation

Canvas

Ein Canvas ist eine View, in die du mit den Operationen der HTML Canvas 2D API zeichnest: Diagramme, Fortschrittsringe, Messinstrumente, Signaturen, eigene Icons, kurz alles, was sich leichter zeichnen als aus Views zusammenbauen lässt. In Rust zeigt canvas(..), was ein Painter aufgezeichnet hat; in TSX nimmt Canvas aus nsh eine draw-Funktion entgegen, die die Zeichnung in einem Durchgang aufzeichnet. Die Plattform spielt sie nativ ab (Core Graphics auf iOS, android.graphics.Canvas auf Android), und zwar in der Auflösung des Bildschirms, sodass Zeichnungen bei jeder Pixeldichte scharf bleiben.

guide-canvas.tsx
guide-canvas.tsxLive
import { Canvas, type GestureResponderEvent, mount, Path2D, signal, Text, View } from "nsh";
import { createAnimatedValue, Easing } from "nsh";
import { button, buttonText, Inspector, Panel, tint, ui, wide } from "./palette";

const TAU = Math.PI * 2;
const heart = new Path2D("M12 21s-7-4.35-9.5-8.5C.5 8.5 3 4 7 4c2 0 3.5 1 5 3 1.5-2 3-3 5-3 4 0 6.5 4.5 4.5 8.5C19 16.65 12 21 12 21z");

function Button(props: { label: string; onPress: () => void }) {
  return (
    <View onPress={props.onPress} {...button()}>
      <Text style={buttonText()}>{props.label}</Text>
    </View>
  );
}

function App() {
  // The ring's sweep is an animated value: animating it redraws without running `draw`.
  const progress = createAnimatedValue(0.35);
  const target = signal(0.35);
  const go = () => {
    const to = Math.round(Math.random() * 100) / 100;
    target.set(to);
    progress.timing(to, { duration: 800, easing: Easing.inOut(Easing.cubic) });
  };

  // The chart reads a signal: changing it runs `draw` again.
  const data = signal([12, 30, 22, 48, 36, 60, 52, 75]);

  // Finger drawing through the responder system.
  const strokes = signal<[number, number][][]>([]);
  const point = (e: GestureResponderEvent): [number, number] => [e.locationX, e.locationY];

  const ring = () => (
    <Canvas
      style={{ width: 110, height: 110 }}
      draw={(cx, { width, height }) => {
        const centerX = width / 2;
        const cy = height / 2;
        const r = width / 2 - 8;
        cx.lineWidth = 10;
        cx.lineCap = "round";
        cx.strokeStyle = ui().border;
        cx.beginPath();
        cx.arc(centerX, cy, r, 0, TAU);
        cx.stroke();
        const g = cx.createConicGradient(-Math.PI / 2, centerX, cy);
        g.addColorStop(0, ui().up);
        g.addColorStop(1, ui().accent);
        cx.strokeStyle = g;
        cx.beginPath();
        cx.arc(centerX, cy, r, -Math.PI / 2, progress.mul(TAU).sub(Math.PI / 2));
        cx.stroke();
        cx.fillStyle = ui().text;
        cx.font = "600 18px system-ui";
        cx.textAlign = "center";
        cx.textBaseline = "middle";
        cx.fillText(`${Math.round(target() * 100)}%`, centerX, cy);
      }}
    />
  );
  const heartCanvas = () => (
    <Canvas
      style={{ width: 48, height: 48 }}
      draw={(cx) => {
        cx.scale(2, 2);
        cx.shadowColor = tint(ui().down, 0.45);
        cx.shadowBlur = 4;
        cx.fillStyle = ui().down;
        cx.fill(heart);
      }}
    />
  );
  const chart = (height: number) => (
    <Canvas
      style={{ ...ui().inset, height, borderRadius: 12 }}
      draw={(cx, { width, height }) => {
        const values = data();
        const pad = 16;
        const x = (i: number) => pad + (i / (values.length - 1)) * (width - 2 * pad);
        const y = (v: number) => height - pad - (v / 100) * (height - 2 * pad);
        const area = cx.createLinearGradient(0, pad, 0, height - pad);
        area.addColorStop(0, tint(ui().accent, 0.28));
        area.addColorStop(1, tint(ui().accent, 0));
        cx.beginPath();
        values.forEach((v, i) => (i === 0 ? cx.moveTo(x(i), y(v)) : cx.lineTo(x(i), y(v))));
        cx.lineTo(x(values.length - 1), height - pad);
        cx.lineTo(x(0), height - pad);
        cx.closePath();
        cx.fillStyle = area;
        cx.fill();
        cx.beginPath();
        values.forEach((v, i) => (i === 0 ? cx.moveTo(x(i), y(v)) : cx.lineTo(x(i), y(v))));
        cx.strokeStyle = ui().accent;
        cx.lineWidth = 2.5;
        cx.lineJoin = "round";
        cx.stroke();
      }}
    />
  );
  const newData = () => <Button label="New data" onPress={() => data.set(data().map(() => Math.round(10 + Math.random() * 85)))} />;
  const pad = () => (
    <Canvas
      style={{ ...ui().inset, flex: 1, borderRadius: 12 }}
      onStartShouldSetResponder={() => true}
      onResponderGrant={(e) => {
        strokes.set([...strokes(), [point(e)]]);
        return true;
      }}
      onResponderMove={(e) => {
        const all = strokes();
        strokes.set([...all.slice(0, -1), [...all[all.length - 1], point(e)]]);
      }}
      draw={(cx, { width }) => {
        cx.fillStyle = ui().faint;
        cx.font = "13px system-ui";
        cx.textAlign = "center";
        cx.fillText("Draw here", width / 2, 20);
        cx.strokeStyle = ui().text;
        cx.lineWidth = 3;
        cx.lineCap = "round";
        cx.lineJoin = "round";
        for (const s of strokes()) {
          cx.beginPath();
          s.forEach(([px, py], i) => (i === 0 ? cx.moveTo(px, py) : cx.lineTo(px, py)));
          cx.stroke();
        }
      }}
    />
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 14 }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <View style={{ flex: 1, minWidth: 0, gap: 14 }}>
              {chart(200)}
              {pad()}
            </View>
            <Inspector>
              <Panel title="An animated value in draw">
                <View style={{ gap: 10 }}>
                  <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
                    {ring()}
                    {heartCanvas()}
                  </View>
                  <Button label="Animate the ring" onPress={go} />
                </View>
              </Panel>
              <Panel title="A signal in draw">
                <View style={{ gap: 10 }}>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>[{data().join(", ")}]</Text>
                  {newData()}
                </View>
              </Panel>
              <Panel title="Responder events">
                <View style={{ gap: 10 }}>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
                    {strokes().length} strokes · {strokes().reduce((n, s) => n + s.length, 0)} points
                  </Text>
                  <Button label="Clear" onPress={() => strokes.set([])} />
                </View>
              </Panel>
            </Inspector>
          </View>
        ) : (
          <>
            <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
              {ring()}
              <View style={{ gap: 10 }}>
                <Button label="Animate the ring" onPress={go} />
                {heartCanvas()}
              </View>
            </View>
            {chart(150)}
            {newData()}
            {pad()}
          </>
        )
      }
    </View>
  );
}

mount(App);
use std::f32::consts::TAU;

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    let area = signal((0.0_f32, 0.0_f32));
    view().flex_1().p_6().pt(px(60.0)).child(
        canvas(move || {
            let (w, h) = area.get();
            Painter::new()
                .fill_color(rgb(0x0969da))
                .begin_path()
                .arc(w / 2.0, h / 2.0, 60.0, 0.0, TAU)
                .fill()
                .stroke_color(rgb(0x1f2328))
                .line_width(4.0)
                .stroke_rect(bounds(point(px(20.0), px(20.0)), size(px(w - 40.0), px(h - 40.0))))
        })
        .h(px(200.0))
        .rounded(px(12.0))
        .bg(white())
        .on_layout(move |frame: &Bounds<Points>| area.set((frame.size.width.0, frame.size.height.0))),
    )
}

Ein Canvas unterstützt alles, was eine View unterstützt (Styles, Klassen, Touch-Handler, Accessibility, Refs), und zusätzlich seine Zeichnung. Er braucht eine Größe aus seinem Style oder Layout; sein Inhalt ist die Zeichnung. Koordinaten werden in Punkten angegeben, der Ursprung liegt oben links.

#Wann die Zeichnung erstellt wird

In Rust zeichnet canvas(move || ..) in einem Binding auf: Die Closure läuft einmal und erneut, sobald sich ein Signal ändert, das sie gelesen hat. Eine Zeichnung, die von der Größe des Canvas abhängt, hält die Größe aus .on_layout(..) in einem Signal und liest sie, wie oben gezeigt. In TSX läuft draw(cx, { width, height }):

  • sobald die Größe des Canvas bekannt ist (nach seinem ersten Layout);
  • erneut, wenn die Größe des Canvas geändert wird;
  • erneut, wenn sich ein Signal ändert, das gelesen wurde.

Jeder Durchlauf ersetzt die gesamte Zeichnung. Indem du beim Zeichnen Signale liest, folgt ein Canvas deinen Daten:

let values = signal(vec![12.0_f32, 30.0, 22.0, 48.0, 36.0, 60.0]);
let width = signal(0.0_f32);
let height = 160.0;

canvas(move || {
    let w = width.get();
    values.with(|v| {
        let x = |i: usize| i as f32 / (v.len() - 1) as f32 * w;
        let y = |n: f32| height - n / 100.0 * height;
        let mut p = Painter::new().begin_path().move_to(x(0), y(v[0]));
        for (i, &n) in v.iter().enumerate().skip(1) {
            p = p.line_to(x(i), y(n));
        }
        p.stroke_color(rgb(0x3b82f6)).line_width(2.5).line_join(LineJoin::Round).stroke()
    })
})
.h(px(height))
.on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))

Zeichenaufrufe zeichnen nur auf: Die Zeichnung wird von der Plattform erstellt, nachdem die Aufzeichnung abgeschlossen ist, daher werden keine Pixel zurückgelesen. Die Aufzeichnung wird als eine einzige Operation an die Plattform gesendet.

#Die 2D-Operationen

Die Operationen sind eine Teilmenge der HTML Canvas 2D API: CanvasRenderingContext2D in TSX und die Methoden von Painter in Rust, mit Rust-Namen (begin_path, fill_rect, line_width, set_transform, draw_image, …; Farben über fill_color / stroke_color, Verläufe über fill_style / stroke_style). Unterstützt werden:

Zustand

Member Rust Beschreibung
save(), restore() .save(), .restore() Zeichenzustand (Styles, Transformation, Clip) auf den Stack legen und wieder entfernen.
fillStyle, strokeStyle .fill_color(..), .fill_style(..), .stroke_color(..), .stroke_style(..) Eine CSS-Farbe, ein Verlauf oder ein animierter Wert, der Farben interpoliert.
lineWidth, lineCap, lineJoin, miterLimit .line_width(..), .line_cap(..), .line_join(..), .miter_limit(..) Form der Linie.
setLineDash(segments), getLineDash(), lineDashOffset .line_dash(..), .line_dash_offset(..) Strichmuster.
globalAlpha .global_alpha(..) Deckkraft dessen, was als Nächstes gezeichnet wird, 0..1.
globalCompositeOperation .composite(Composite::DestinationOut) Wie es sich verbindet: "source-over", "destination-out", "multiply", "screen", …
shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY .shadow(color, blur, offset) Schatten.
font .font(..) CSS-Kurzschreibweise font: "600 14px Inter" (Größen in px, pt oder rem); small-caps und die Zahlenformen tabular-nums, oldstyle-nums, … dürfen vor der Größe stehen.
fontVariantNumeric .font_variant_numeric(..) "tabular-nums" gibt allen Ziffern dieselbe Breite, sodass sich ändernde Zahlen (Zähler, Preise, Achsenbeschriftungen) nicht springen; außerdem "proportional-nums", "oldstyle-nums", "lining-nums", "normal". Eine Ergänzung von NSH (CSS kennt font-variant-numeric); das Setzen von font behält sie bei. Browser ignorieren sie.
textAlign, textBaseline .text_align(..), .text_baseline(..) Textplatzierung.

Transformationen: translate(x, y), scale(x, y), rotate(angle) (Bogenmaß, im Uhrzeigersinn), transform(a, b, c, d, e, f), setTransform(a, b, c, d, e, f), resetTransform().

Pfade: beginPath(), moveTo, lineTo, quadraticCurveTo, bezierCurveTo, arc, arcTo, ellipse, rect, roundRect(x, y, w, h, radii), closePath(); dann fill(rule?), stroke() und clip(rule?), die jeweils auch einen Pfad entgegennehmen (fill_path, stroke_path, clip_path in Rust). Füllregeln sind "nonzero" und "evenodd".

Rechtecke: fillRect, strokeRect, clearRect.

Text: fillText(text, x, y, maxWidth?), strokeText(...), measureText(text) (synchron von der Text-Engine der Plattform mit dem aktuellen font gemessen; nur width). In Rust liefert measure_text(font, text) die Breite.

Hit-Testing: isPointInPath(path?, x, y, rule?) und isPointInStroke(path?, x, y) (path_contains, stroke_contains, is_point_in_path und is_point_in_stroke auf einem Painter) antworten synchron und werden aus der aufgezeichneten Geometrie berechnet (aktuelle Transformation, Pfad, lineWidth und lineCap). Behalte die Zeichnung und die gezeichneten Pfade, um die Form unter einem Tap zu finden:

let slices: Rc<Vec<Path>> = Rc::new(vec![svg_path("M50 50 L100 50 A50 50 0 0 1 50 100 Z") /* , ... */]);
let draw = {
    let slices = slices.clone();
    move || slices.iter().fold(Painter::new(), |p, s| p.fill_path(s, FillRule::NonZero))
};

canvas(draw.clone())
    .size(px(100.0))
    .claim_touches(TouchClaim::on_start())
    .on_responder_release(move |e: &ResponderEvent| {
        let drawing = draw();
        if let Some(hit) = slices.iter().position(|s| drawing.path_contains(s, e.location, FillRule::NonZero)) {
            selected.set(Some(hit));
        }
    })

Linien werden mit runden Verbindungen und ohne Strichmuster getestet; Koordinaten, die als animierte Werte angegeben sind, zählen als 0.

Verläufe: createLinearGradient, createRadialGradient, createConicGradient, jeweils mit addColorStop(offset, color) (CanvasGradient::linear, radial, conic und add_color_stop in Rust).

Bilder: drawImage(image, dx, dy), drawImage(image, dx, dy, dw, dh), drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) (draw_image(url, dest) und draw_image_part(..) in Rust).

cx.canvas.width und cx.canvas.height liefern in TSX die Größe des Canvas in Punkten.

Die unterstützte Menge: Zeichenoperationen. Pixel werden von der Plattform geschrieben, daher gehören getImageData / putImageData und anderer Pixelzugriff nicht dazu, ebenso wenig filter und SVG-Bilder in drawImage. Eine ungültige Farbe oder Schrift verwirft genau diese eine Operation, wie bei HTML Canvas.

#Pfade und SVG-Pfaddaten

Ein einmal erstellter Pfad lässt sich beliebig oft füllen, zeichnen oder als Clip verwenden: svg_path(d) in Rust, Path2D in TSX. Beide nehmen SVG-Pfaddaten entgegen, die nativ geparst werden, was Icons einfach macht:

use nsh_core::canvas::{FillRule, svg_path};

const HEART: &str = "M12 21s-7-4.35-9.5-8.5C.5 8.5 3 4 7 4c2 0 3.5 1 5 3 1.5-2 3-3 5-3 4 0 6.5 4.5 4.5 8.5C19 16.65 12 21 12 21z";

fn heart(size: f32, color: Rgba) -> impl IntoElement {
    let path = svg_path(HEART);
    canvas(
        Painter::new()
            .scale(size / 24.0, size / 24.0) // the path is drawn on a 24-point grid
            .fill_color(color)
            .fill_path(&path, FillRule::NonZero),
    )
    .size(px(size))
}

Ein Path2D hat dieselben Pfadmethoden wie der Kontext (moveTo, arc, roundRect, …) und addPath(other). new Path2D(other) kopiert einen Pfad.

#Verläufe

canvas(move || {
    let (w, h) = (width.get(), 120.0);
    let linear = CanvasGradient::linear(point(px(0.0), px(0.0)), point(px(w), px(0.0)))
        .add_color_stop(percentage(0.0), rgb(0x22d3ee))
        .add_color_stop(percentage(1.0), rgb(0x6366f1));
    let center = point(px(w / 2.0), px(h * 0.75));
    let radial = CanvasGradient::radial(center, px(4.0), center, px(40.0))
        .add_color_stop(percentage(0.0), rgb(0xfde68a))
        .add_color_stop(percentage(1.0), rgb(0xf97316));
    Painter::new()
        .fill_style(linear)
        .fill_rect(0.0, 0.0, w, h / 2.0)
        .fill_style(radial)
        .begin_path()
        .arc(w / 2.0, h * 0.75, 28.0, 0.0, TAU)
        .fill()
})
.h(px(120.0))
.on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))

Ein konischer Verlauf (createConicGradient(startAngle, x, y), CanvasGradient::conic(start_angle, center)) lässt Farben um einen Punkt kreisen; sein Startwinkel ist in Bogenmaß angegeben, im Uhrzeigersinn ab der x-Achse. Ein konischer Verlauf auf einem gezeichneten Bogen ergibt einen Fortschrittsring mit Farbverlauf.

#Text

Painter::new()
    .font("600 20px system-ui")
    .fill_color(rgb(0x1f2328))
    .text_align(CanvasTextAlign::Center)
    .text_baseline(CanvasTextBaseline::Middle)
    .fill_text("72%", width / 2.0, height / 2.0)
    .font("11px system-ui")
    .fill_text("100", 32.0 - 6.0 - measure_text("11px system-ui", "100").0, 16.0)

Schriftfamilien sind dieselben wie in Styles: Systemfamilien und die Namen der Schriften, die deine App mitliefert (siehe Fonts).

#Bilder

Zum Zeichnen eines Bildes wird in Rust eine URL verwendet (die eines Assets ist LOGO.url), in TSX eine URL oder ein importiertes Asset (import logo from "./logo.png"). Das Bild wird nativ geladen, und der Canvas zeichnet erneut, sobald es geladen ist, sodass du den Ladezustand nicht selbst verfolgen musst. Für Remote-URLs gelten die Netzwerk-Capabilities der App.

Canvases auf iOS, macOS, Android, Windows, Linux und GPUI zeichnen Bitmap-Bilder (PNG, JPEG, GIF, WebP, …); auf diesen Plattformen ist eine SVG-Quelle ein Bild, das nicht geladen werden kann, und das Log nennt sie. Zeige SVG-Bilder mit <Image> an oder zeichne ihre Formen als Path2D-Pfade. Im Web zeichnet ein Canvas, was der Browser zeichnet, einschließlich SVG.

const LOGO: ImageAsset = asset!("logo.png");

canvas(
    Painter::new()
        .save()
        .begin_path()
        .arc(60.0, 60.0, 56.0, 0.0, TAU)
        .clip() // a round avatar
        .draw_image(LOGO.url, bounds(point(px(4.0), px(4.0)), size(px(112.0), px(112.0))))
        .restore(),
)
.size(px(120.0))

Die Form mit neun Argumenten (draw_image_part) nimmt das Quellrechteck in den Pixeln des Bildes entgegen.

#Zeichnungen mit animierten Werten steuern

Jedes Zahlenargument eines Zeichenaufrufs sowie die Füll- und Linienfarben können ein animierter Wert oder ein daraus abgeleiteter Knoten sein. Solche Argumente werden gelesen, während die Plattform zeichnet, und wenn der Wert animiert wird, zeichnet die Plattform von selbst in jedem Frame neu, ohne die Zeichnung erneut aufzuzeichnen und ohne deinen Code auszuführen.

use std::f32::consts::{FRAC_PI_2, TAU};

use nsh_core::{
    canvas::{CanvasTextAlign, CanvasTextBaseline, LineCap},
    prelude::*,
};

pub fn ring() -> impl IntoElement {
    let progress = animated_value(0.3);
    let label = signal(30_u32);
    let go = move |to: f32| {
        label.set((to * 100.0).round() as u32);
        progress.timing(to, 900.0, Easing::Curve(Curve::Cubic, EaseMode::InOut));
    };
    let (center_x, cy, r) = (70.0, 70.0, 60.0);
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_4()
        .child(
            canvas(move || {
                // The end angle reads the animated value on every frame.
                let end = progress.mul(TAU).sub(FRAC_PI_2);
                Painter::new()
                    .line_width(12.0)
                    .line_cap(LineCap::Round)
                    .stroke_color(rgb(0xe5e7eb))
                    .begin_path()
                    .arc(center_x, cy, r, 0.0, TAU)
                    .stroke()
                    .stroke_color(rgb(0x6366f1))
                    .begin_path()
                    .arc(center_x, cy, r, -FRAC_PI_2, &end)
                    .stroke()
                    .fill_color(rgb(0x1f2328))
                    .font("600 22px system-ui")
                    .text_align(CanvasTextAlign::Center)
                    .text_baseline(CanvasTextBaseline::Middle)
                    .fill_text(&format!("{}%", label.get()), center_x, cy)
            })
            .size(px(140.0)),
        )
        .child(
            text("Animate")
                .text_color(rgb(0x0969da))
                .on_press(move || go(if label.get_untracked() < 50 { 0.85 } else { 0.2 })),
        )
}

Der Endwinkel des Bogens ist progress.mul(2π).sub(π/2): ein Knoten, sodass der Ring 900 ms lang nativ überstreicht, während die Zeichnung nur einmal aufgezeichnet wurde (und einmal mehr für das Label, das ein Signal liest). Der Sekundenzeiger einer Uhr funktioniert genauso: rotate(seconds.mul(π / 30)) mit linear animiertem seconds.

Farben werden auf dieselbe Weise animiert: .fill_style(&t.interpolate_colors(&[0.0, 1.0], &[rgb(0x54aeff), rgb(0xcf222e)])) in Rust, cx.fillStyle = t.interpolate({ inputRange: [0, 1], outputRange: ["#54aeff", "#cf222e"] }) in TSX.

#Touch auf einem Canvas

Ein Canvas ist eine View und unterstützt daher Presses und das Responder-System. Beim Zeichnen mit dem Finger werden Punkte in einem Signal gespeichert und gezeichnet:

let strokes = signal(Vec::<Vec<(f32, f32)>>::new());

canvas(move || {
    strokes.with(|strokes| {
        let mut p = Painter::new().line_width(3.0).line_cap(LineCap::Round).line_join(LineJoin::Round);
        for stroke in strokes {
            p = p.begin_path();
            for (i, &(x, y)) in stroke.iter().enumerate() {
                p = if i == 0 { p.move_to(x, y) } else { p.line_to(x, y) };
            }
            p = p.stroke();
        }
        p
    })
})
.h(px(240.0))
.claim_touches(TouchClaim::on_start())
.keep_touches(true) // a scroll view around must not take the touches
.on_responder_grant(move |e: &ResponderEvent| {
    strokes.update(|s| s.push(vec![(e.location.x.0, e.location.y.0)]))
})
.on_responder_move(move |e: &ResponderEvent| {
    strokes.update(|s| {
        if let Some(stroke) = s.last_mut() {
            stroke.push((e.location.x.0, e.location.y.0));
        }
    })
})

#Performance

  • Eine Zeichnung wird in Rust aufgezeichnet (in TSX als JS-Aufrufe auf den Kontext) und als eine einzige Operation an die Plattform übertragen; es gibt keinen Bridge-Übergang pro Aufruf.
  • Jede Zeichnung zeichnet alles neu auf. Halte den Aufwand proportional zu dem, was angezeigt wird, und halte Daten, die sich in jedem Frame ändern, in animierten Werten statt in Signalen.
  • Teile eine Zeichnung mit einem statischen und einem häufig wechselnden Teil auf zwei übereinandergelegte Canvases auf, sodass nur einer neu zeichnet.
  • In einer zoomenden Scroll-View werden Canvases in der gezoomten Auflösung neu gezeichnet, wenn der Zoom endet.

#Siehe auch