diff options
| author | Laurenz <laurmaedje@gmail.com> | 2020-08-02 21:17:42 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2020-08-02 21:17:42 +0200 |
| commit | cbbc46215fe0a0ad8a50e991ec442890b8eadc0a (patch) | |
| tree | 2efbac21cec46787f1efe0a859564b9614eefa98 /src | |
| parent | d5ff97f42ed1e682a66ea8d51e5f9ed1be547b9c (diff) | |
Layout elements and pure rust rendering 🥏
Diffstat (limited to 'src')
| -rw-r--r-- | src/export/pdf.rs | 66 | ||||
| -rw-r--r-- | src/font.rs | 14 | ||||
| -rw-r--r-- | src/geom.rs | 5 | ||||
| -rw-r--r-- | src/layout/actions.rs | 166 | ||||
| -rw-r--r-- | src/layout/elements.rs | 84 | ||||
| -rw-r--r-- | src/layout/line.rs | 12 | ||||
| -rw-r--r-- | src/layout/mod.rs | 28 | ||||
| -rw-r--r-- | src/layout/model.rs | 3 | ||||
| -rw-r--r-- | src/layout/stack.rs | 17 | ||||
| -rw-r--r-- | src/layout/text.rs | 52 | ||||
| -rw-r--r-- | src/lib.rs | 12 | ||||
| -rw-r--r-- | src/library/layout.rs | 6 |
12 files changed, 156 insertions, 309 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs index f06e06af..71665149 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -15,7 +15,8 @@ use fontdock::FaceId; use ttf_parser::{name_id, GlyphId}; use crate::SharedFontLoader; -use crate::layout::{MultiLayout, Layout, LayoutAction}; +use crate::layout::{MultiLayout, Layout}; +use crate::layout::elements::LayoutElement; use crate::length::Length; /// Export a layouted list of boxes. The same font loader as used for @@ -144,38 +145,26 @@ impl<'a, W: Write> PdfExporter<'a, W> { // Moves and face switches are always cached and only flushed once // needed. let mut text = Text::new(); - let mut face_id = FaceId::MAX; - let mut font_size = 0.0; - let mut next_pos = None; - - for action in &page.actions { - match action { - LayoutAction::MoveAbsolute(pos) => { - next_pos = Some(*pos); - }, - - &LayoutAction::SetFont(id, size) => { - face_id = id; - font_size = size; - text.tf( - self.to_pdf[&id] as u32 + 1, - Length::raw(font_size).as_pt() as f32 - ); - } - - LayoutAction::WriteText(string) => { - if let Some(pos) = next_pos.take() { - let x = Length::raw(pos.x).as_pt(); - let y = Length::raw(page.dimensions.y - pos.y - font_size).as_pt(); - text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32); + let mut face = FaceId::MAX; + let mut size = 0.0; + + for (pos, element) in &page.elements.0 { + match element { + LayoutElement::Text(shaped) => { + if shaped.face != face || shaped.size != size { + face = shaped.face; + size = shaped.size; + text.tf( + self.to_pdf[&shaped.face] as u32 + 1, + Length::raw(size).as_pt() as f32 + ); } - let loader = self.loader.borrow(); - let face = loader.get_loaded(face_id); - text.tj(face.encode_text(&string)); - }, - - LayoutAction::DebugBox(_) => {} + let x = Length::raw(pos.x).as_pt(); + let y = Length::raw(page.dimensions.y - pos.y - size).as_pt(); + text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32); + text.tj(shaped.encode_glyphs()); + } } } @@ -313,14 +302,13 @@ fn remap_fonts(layouts: &MultiLayout) -> (HashMap<FaceId, usize>, Vec<FaceId>) { // We want to find out which fonts are used at all. To do that, look at each // text element to find out which font is uses. for layout in layouts { - for action in &layout.actions { - if let &LayoutAction::SetFont(id, _) = action { - to_pdf.entry(id).or_insert_with(|| { - let next_id = to_fontdock.len(); - to_fontdock.push(id); - next_id - }); - } + for (_, element) in &layout.elements.0 { + let LayoutElement::Text(shaped) = element; + to_pdf.entry(shaped.face).or_insert_with(|| { + let next_id = to_fontdock.len(); + to_fontdock.push(shaped.face); + next_id + }); } } diff --git a/src/font.rs b/src/font.rs index 81a63445..5d81b6fc 100644 --- a/src/font.rs +++ b/src/font.rs @@ -38,20 +38,6 @@ impl OwnedFace { pub fn data(&self) -> &[u8] { &self.data } - - /// Encode the text into glyph ids and encode these into a big-endian byte - /// buffer. - pub fn encode_text(&self, text: &str) -> Vec<u8> { - const BYTES_PER_GLYPH: usize = 2; - let mut bytes = Vec::with_capacity(BYTES_PER_GLYPH * text.len()); - for c in text.chars() { - if let Some(glyph) = self.glyph_index(c) { - bytes.push((glyph.0 >> 8) as u8); - bytes.push((glyph.0 & 0xff) as u8); - } - } - bytes - } } impl ContainsChar for OwnedFace { diff --git a/src/geom.rs b/src/geom.rs index f5f818a4..5e65446e 100644 --- a/src/geom.rs +++ b/src/geom.rs @@ -3,14 +3,10 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::*; -#[cfg(feature = "serialize")] -use serde::Serialize; - use crate::layout::prelude::*; /// A value in two dimensions. #[derive(Default, Copy, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serialize", derive(Serialize))] pub struct Value2<T> { /// The horizontal component. pub x: T, @@ -180,7 +176,6 @@ impl Neg for Size { /// A value in four dimensions. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serialize", derive(Serialize))] pub struct Value4<T> { /// The left extent. pub left: T, diff --git a/src/layout/actions.rs b/src/layout/actions.rs deleted file mode 100644 index 7a32a46a..00000000 --- a/src/layout/actions.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Drawing and configuration actions composing layouts. - -use std::fmt::{self, Debug, Formatter}; - -#[cfg(feature = "serialize")] -use serde::ser::{Serialize, Serializer, SerializeTuple}; - -use fontdock::FaceId; -use crate::geom::Size; -use super::Layout; -use self::LayoutAction::*; - -/// A layouting action, which is the basic building block layouts are composed -/// of. -#[derive(Clone, PartialEq)] -pub enum LayoutAction { - /// Move to an absolute position. - MoveAbsolute(Size), - /// Set the font given the index from the font loader and font size. - SetFont(FaceId, f64), - /// Write text at the current position. - WriteText(String), - /// Visualize a box for debugging purposes. - DebugBox(Size), -} - -#[cfg(feature = "serialize")] -impl Serialize for LayoutAction { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer { - match self { - LayoutAction::MoveAbsolute(pos) => { - let mut tup = serializer.serialize_tuple(2)?; - tup.serialize_element(&0u8)?; - tup.serialize_element(&pos)?; - tup.end() - } - LayoutAction::SetFont(id, size) => { - let mut tup = serializer.serialize_tuple(4)?; - tup.serialize_element(&1u8)?; - tup.serialize_element(id)?; - tup.serialize_element(size)?; - tup.end() - } - LayoutAction::WriteText(text) => { - let mut tup = serializer.serialize_tuple(2)?; - tup.serialize_element(&2u8)?; - tup.serialize_element(text)?; - tup.end() - } - LayoutAction::DebugBox(size) => { - let mut tup = serializer.serialize_tuple(2)?; - tup.serialize_element(&3u8)?; - tup.serialize_element(&size)?; - tup.end() - } - } - } -} - -impl Debug for LayoutAction { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - use LayoutAction::*; - match self { - MoveAbsolute(s) => write!(f, "move {} {}", s.x, s.y), - SetFont(id, s) => write!(f, "font {}-{} {}", id.index, id.variant, s), - WriteText(s) => write!(f, "write {:?}", s), - DebugBox(s) => write!(f, "box {} {}", s.x, s.y), - } - } -} - -/// A sequence of layouting actions. -/// -/// The sequence of actions is optimized as the actions are added. For example, -/// a font changing option will only be added if the selected font is not -/// already active. All configuration actions (like moving, setting fonts, ...) -/// are only flushed when content is written. -/// -/// Furthermore, the action list can translate absolute position into a -/// coordinate system with a different origin. This is realized in the -/// `add_layout` method, which allows a layout to be added at a position, -/// effectively translating all movement actions inside the layout by the -/// position. -#[derive(Debug, Clone, PartialEq)] -pub struct LayoutActions { - origin: Size, - actions: Vec<LayoutAction>, - active_font: (FaceId, f64), - next_pos: Option<Size>, - next_font: Option<(FaceId, f64)>, -} - -impl LayoutActions { - /// Create a new action list. - pub fn new() -> LayoutActions { - LayoutActions { - actions: vec![], - origin: Size::ZERO, - active_font: (FaceId::MAX, 0.0), - next_pos: None, - next_font: None, - } - } - - /// Add an action to the list. - pub fn add(&mut self, action: LayoutAction) { - match action { - MoveAbsolute(pos) => self.next_pos = Some(self.origin + pos), - SetFont(index, size) => { - self.next_font = Some((index, size)); - } - - _ => { - self.flush_position(); - self.flush_font(); - - self.actions.push(action); - } - } - } - - /// Add a series of actions. - pub fn extend<I>(&mut self, actions: I) where I: IntoIterator<Item = LayoutAction> { - for action in actions.into_iter() { - self.add(action); - } - } - - /// Add a layout at a position. All move actions inside the layout are - /// translated by the position. - pub fn add_layout(&mut self, position: Size, layout: Layout) { - self.flush_position(); - - self.origin = position; - self.next_pos = Some(position); - - self.extend(layout.actions); - } - - /// Whether there are any actions in this list. - pub fn is_empty(&self) -> bool { - self.actions.is_empty() - } - - /// Return the list of actions as a vector. - pub fn into_vec(self) -> Vec<LayoutAction> { - self.actions - } - - /// Append a cached move action if one is cached. - fn flush_position(&mut self) { - if let Some(target) = self.next_pos.take() { - self.actions.push(MoveAbsolute(target)); - } - } - - /// Append a cached font-setting action if one is cached. - fn flush_font(&mut self) { - if let Some((index, size)) = self.next_font.take() { - if (index, size) != self.active_font { - self.actions.push(SetFont(index, size)); - self.active_font = (index, size); - } - } - } -} diff --git a/src/layout/elements.rs b/src/layout/elements.rs new file mode 100644 index 00000000..e524e1fd --- /dev/null +++ b/src/layout/elements.rs @@ -0,0 +1,84 @@ +//! The elements layouts are composed of. + +use std::fmt::{self, Debug, Formatter}; + +use ttf_parser::GlyphId; +use fontdock::FaceId; +use crate::geom::Size; + +/// A sequence of positioned layout elements. +#[derive(Debug, Clone, PartialEq)] +pub struct LayoutElements(pub Vec<(Size, LayoutElement)>); + +impl LayoutElements { + /// Create an empty sequence. + pub fn new() -> Self { + LayoutElements(vec![]) + } + + /// Add an element at a position. + pub fn push(&mut self, pos: Size, element: LayoutElement) { + self.0.push((pos, element)); + } + + /// Add a sequence of elements offset by an `offset`. + pub fn extend_offset(&mut self, offset: Size, more: Self) { + for (subpos, element) in more.0 { + self.0.push((subpos + offset, element)); + } + } +} + +impl Default for LayoutElements { + fn default() -> Self { + Self::new() + } +} + +/// A layouting action, which is the basic building block layouts are composed +/// of. +#[derive(Debug, Clone, PartialEq)] +pub enum LayoutElement { + /// Shaped text. + Text(Shaped), +} + +/// A shaped run of text. +#[derive(Clone, PartialEq)] +pub struct Shaped { + pub text: String, + pub face: FaceId, + pub glyphs: Vec<GlyphId>, + pub offsets: Vec<f64>, + pub size: f64, +} + +impl Shaped { + /// Create an empty shape run. + pub fn new(face: FaceId, size: f64) -> Shaped { + Shaped { + text: String::new(), + face, + glyphs: vec![], + offsets: vec![], + size, + } + } + + /// Encode the glyph ids into a big-endian byte buffer. + pub fn encode_glyphs(&self) -> Vec<u8> { + const BYTES_PER_GLYPH: usize = 2; + let mut bytes = Vec::with_capacity(BYTES_PER_GLYPH * self.glyphs.len()); + for g in &self.glyphs { + bytes.push((g.0 >> 8) as u8); + bytes.push((g.0 & 0xff) as u8); + } + bytes + } +} + +impl Debug for Shaped { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Shaped({})", self.text) + } +} diff --git a/src/layout/line.rs b/src/layout/line.rs index aafdab60..6b2fd3c6 100644 --- a/src/layout/line.rs +++ b/src/layout/line.rs @@ -35,9 +35,6 @@ pub struct LineContext { pub align: LayoutAlign, /// Whether to have repeated spaces or to use only the first and only once. pub repeat: bool, - /// Whether to output a command which renders a debugging box showing the - /// extent of the layout. - pub debug: bool, /// The line spacing. pub line_spacing: f64, } @@ -73,7 +70,6 @@ impl LineLayouter { axes: ctx.axes, align: ctx.align, repeat: ctx.repeat, - debug: ctx.debug, }), ctx, run: LineRun::new(), @@ -252,9 +248,9 @@ impl LineLayouter { /// Finish the line and start a new one. pub fn finish_line(&mut self) { - let mut actions = LayoutActions::new(); + let mut elements = LayoutElements::new(); - let layouts = std::mem::replace(&mut self.run.layouts, vec![]); + let layouts = std::mem::take(&mut self.run.layouts); for (offset, layout) in layouts { let x = match self.ctx.axes.primary.is_positive() { true => offset, @@ -264,14 +260,14 @@ impl LineLayouter { }; let pos = Size::with_x(x); - actions.add_layout(pos, layout); + elements.extend_offset(pos, layout.elements); } self.stack.add(Layout { dimensions: self.run.size.specialized(self.ctx.axes), align: self.run.align .unwrap_or(LayoutAlign::new(Start, Start)), - actions: actions.into_vec(), + elements }); self.run = LineRun::new(); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 64a2825b..41a314f0 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -2,17 +2,14 @@ use std::fmt::{self, Display, Formatter}; -#[cfg(feature = "serialize")] -use serde::Serialize; - -use fontdock::FaceId; use crate::geom::{Size, Margins}; -use self::prelude::*; +use elements::LayoutElements; +use prelude::*; pub mod line; pub mod stack; pub mod text; -pub_use_mod!(actions); +pub mod elements; pub_use_mod!(model); /// Basic types used across the layouting engine. @@ -33,30 +30,13 @@ pub type MultiLayout = Vec<Layout>; /// A finished box with content at fixed positions. #[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serialize", derive(Serialize))] pub struct Layout { /// The size of the box. pub dimensions: Size, /// How to align this layout in a parent container. - #[cfg_attr(feature = "serialize", serde(skip))] pub align: LayoutAlign, /// The actions composing this layout. - pub actions: Vec<LayoutAction>, -} - -impl Layout { - /// Returns a vector with all used font indices. - pub fn find_used_fonts(&self) -> Vec<FaceId> { - let mut fonts = Vec::new(); - for action in &self.actions { - if let &LayoutAction::SetFont(id, _) = action { - if !fonts.contains(&id) { - fonts.push(id); - } - } - } - fonts - } + pub elements: LayoutElements, } /// A vector of layout spaces, that is stack allocated as long as it only diff --git a/src/layout/model.rs b/src/layout/model.rs index bde451e6..db069870 100644 --- a/src/layout/model.rs +++ b/src/layout/model.rs @@ -46,8 +46,6 @@ pub struct LayoutContext<'a> { /// Whether the layout that is to be created will be nested in a parent /// container. pub nested: bool, - /// Whether to render debug boxs around layouts if `nested` is true. - pub debug: bool, } /// A sequence of layouting commands. @@ -117,7 +115,6 @@ impl<'a> ModelLayouter<'a> { axes: ctx.axes, align: ctx.align, repeat: ctx.repeat, - debug: ctx.debug && ctx.nested, line_spacing: ctx.style.text.line_spacing(), }), style: ctx.style.clone(), diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 48c7b40a..4f4d3d8b 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -48,9 +48,6 @@ pub struct StackContext { pub align: LayoutAlign, /// Whether to have repeated spaces or to use only the first and only once. pub repeat: bool, - /// Whether to output a command which renders a debugging box showing the - /// extent of the layout. - pub debug: bool, } /// A layout space composed of subspaces which can have different axes and @@ -139,7 +136,7 @@ impl StackLayouter { self.space.layouts.push((self.ctx.axes, Layout { dimensions: dimensions.specialized(self.ctx.axes), align: LayoutAlign::new(Start, Start), - actions: vec![] + elements: LayoutElements::new(), })); self.space.last_spacing = LastSpacing::Hard; @@ -367,13 +364,9 @@ impl StackLayouter { // Step 4: Align each layout in its bounding box and collect everything // into a single finished layout. - let mut actions = LayoutActions::new(); + let mut elements = LayoutElements::new(); - if self.ctx.debug { - actions.add(LayoutAction::DebugBox(dimensions)); - } - - let layouts = std::mem::replace(&mut self.space.layouts, vec![]); + let layouts = std::mem::take(&mut self.space.layouts); for ((axes, layout), bound) in layouts.into_iter().zip(bounds) { let size = layout.dimensions.specialized(axes); let align = layout.align; @@ -387,13 +380,13 @@ impl StackLayouter { let local = usable.anchor(align, axes) - size.anchor(align, axes); let pos = Size::new(bound.left, bound.top) + local.specialized(axes); - actions.add_layout(pos, layout); + elements.extend_offset(pos, layout.elements); } self.layouts.push(Layout { dimensions, align: self.ctx.align, - actions: actions.into_vec(), + elements, }); // ------------------------------------------------------------------ // diff --git a/src/layout/text.rs b/src/layout/text.rs index 6698a0fa..477099e2 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -4,10 +4,12 @@ //! When the primary layouting axis horizontally inversed, the word is spelled //! backwards. Vertical word layout is not yet supported. +use ttf_parser::GlyphId; use fontdock::{FaceId, FaceQuery, FontStyle}; use crate::font::SharedFontLoader; use crate::geom::Size; use crate::style::TextStyle; +use super::elements::{LayoutElement, Shaped}; use super::*; /// Performs the text layouting. @@ -15,9 +17,9 @@ use super::*; struct TextLayouter<'a> { ctx: TextContext<'a>, text: &'a str, - actions: LayoutActions, - buffer: String, - active_font: FaceId, + shaped: Shaped, + elements: LayoutElements, + start: f64, width: f64, } @@ -48,9 +50,9 @@ impl<'a> TextLayouter<'a> { TextLayouter { ctx, text, - actions: LayoutActions::new(), - buffer: String::new(), - active_font: FaceId::MAX, + shaped: Shaped::new(FaceId::MAX, ctx.style.font_size()), + elements: LayoutElements::new(), + start: 0.0, width: 0.0, } } @@ -69,45 +71,53 @@ impl<'a> TextLayouter<'a> { } // Flush the last buffered parts of the word. - if !self.buffer.is_empty() { - self.actions.add(LayoutAction::WriteText(self.buffer)); + if !self.shaped.text.is_empty() { + let pos = Size::new(self.start, 0.0); + self.elements.push(pos, LayoutElement::Text(self.shaped)); } Layout { dimensions: Size::new(self.width, self.ctx.style.font_size()), align: self.ctx.align, - actions: self.actions.into_vec(), + elements: self.elements, } } /// Layout an individual character. async fn layout_char(&mut self, c: char) { - let (index, char_width) = match self.select_font(c).await { + let (index, glyph, char_width) = match self.select_font(c).await { Some(selected) => selected, // TODO: Issue warning about missing character. None => return, }; - self.width += char_width; - // Flush the buffer and issue a font setting action if the font differs // from the last character's one. - if self.active_font != index { - if !self.buffer.is_empty() { - let text = std::mem::replace(&mut self.buffer, String::new()); - self.actions.add(LayoutAction::WriteText(text)); + if self.shaped.face != index { + if !self.shaped.text.is_empty() { + let pos = Size::new(self.start, 0.0); + let shaped = std::mem::replace( + &mut self.shaped, + Shaped::new(FaceId::MAX, self.ctx.style.font_size()), + ); + + self.elements.push(pos, LayoutElement::Text(shaped)); + self.start = self.width; } - self.actions.add(LayoutAction::SetFont(index, self.ctx.style.font_size())); - self.active_font = index; + self.shaped.face = index; } - self.buffer.push(c); + self.shaped.text.push(c); + self.shaped.glyphs.push(glyph); + self.shaped.offsets.push(self.width); + + self.width += char_width; } /// Select the best font for a character and return its index along with /// the width of the char in the font. - async fn select_font(&mut self, c: char) -> Option<(FaceId, f64)> { + async fn select_font(&mut self, c: char) -> Option<(FaceId, GlyphId, f64)> { let mut loader = self.ctx.loader.borrow_mut(); let mut variant = self.ctx.style.variant; @@ -140,7 +150,7 @@ impl<'a> TextLayouter<'a> { let glyph_width = face.glyph_hor_advance(glyph)?; let char_width = to_raw(glyph_width) * self.ctx.style.font_size(); - Some((id, char_width)) + Some((id, glyph, char_width)) } else { None } @@ -13,8 +13,7 @@ //! - **Exporting:** The finished layout can then be exported into a supported //! format. Submodules for these formats are located in the //! [export](crate::export) module. Currently, the only supported output -//! format is [_PDF_](crate::export::pdf). Alternatively, the layout can be -//! serialized to pass it to a suitable renderer. +//! format is [_PDF_](crate::export::pdf). use std::fmt::Debug; @@ -62,8 +61,6 @@ pub struct Typesetter { style: LayoutStyle, /// The base parser state. parse_state: ParseState, - /// Whether to render debug boxes. - debug: bool, } impl Typesetter { @@ -73,7 +70,6 @@ impl Typesetter { loader, style: LayoutStyle::default(), parse_state: ParseState { scope: Scope::with_std() }, - debug: false, } } @@ -87,11 +83,6 @@ impl Typesetter { self.style.page = style; } - /// Set whether to render debug boxes. - pub fn set_debug(&mut self, debug: bool) { - self.debug = debug; - } - /// Parse source code into a syntax tree. pub fn parse(&self, src: &str) -> Pass<SyntaxModel> { parse(src, Pos::ZERO, &self.parse_state) @@ -117,7 +108,6 @@ impl Typesetter { axes: LayoutAxes::new(LTT, TTB), align: LayoutAlign::new(Start, Start), nested: false, - debug: self.debug, }, ).await } diff --git a/src/library/layout.rs b/src/library/layout.rs index ed936816..5dd754bb 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -8,7 +8,6 @@ function! { body: SyntaxModel, width: Option<ScaleLength>, height: Option<ScaleLength>, - debug: Option<bool>, } parse(header, body, ctx, f) { @@ -16,7 +15,6 @@ function! { body: body!(opt: body, ctx, f).unwrap_or(SyntaxModel::new()), width: header.args.key.get::<ScaleLength>("width", f), height: header.args.key.get::<ScaleLength>("height", f), - debug: header.args.key.get::<bool>("debug", f), } } @@ -24,10 +22,6 @@ function! { ctx.repeat = false; ctx.spaces.truncate(1); - if let Some(debug) = self.debug { - ctx.debug = debug; - } - self.width.with(|v| { let length = v.raw_scaled(ctx.base.x); ctx.base.x = length; |
