diff --git a/soft-skia-wasm/src/lib.rs b/soft-skia-wasm/src/lib.rs index 9a76255..d38c16c 100644 --- a/soft-skia-wasm/src/lib.rs +++ b/soft-skia-wasm/src/lib.rs @@ -1,13 +1,11 @@ extern crate soft_skia; mod utils; -use std::collections::HashMap; - use base64; -use soft_skia::provider::{Providers, Group, Provider, GroupClip}; +use soft_skia::provider::{Providers, Group, GroupClip}; use wasm_bindgen::prelude::*; use soft_skia::instance::Instance; -use soft_skia::shape::{Circle, Line, Points, RoundRect, Shapes, PaintStyle, DrawContext, Image}; +use soft_skia::shape::{Circle, Line, Points, RoundRect, Shapes, PaintStyle, Image, Text}; use soft_skia::shape::Rect; use soft_skia::shape::ColorU8; use soft_skia::tree::Node; @@ -96,6 +94,15 @@ pub struct WASMImageAttr { height: u32, } +#[derive(Serialize, Deserialize, Debug)] +pub struct WASMTextAttr { + text: String, + x: i32, + y: i32, + font_size: Option, + color: Option +} + #[derive(Serialize, Deserialize, Debug)] pub enum WASMShapesAttr { @@ -106,7 +113,8 @@ pub enum WASMShapesAttr { P(WASMPointsAttr), G(WASMGroupAttr), GC(WASMGroupClipAttr), - I(WASMImageAttr) + I(WASMImageAttr), + T(WASMTextAttr), } #[derive(Serialize, Deserialize, Debug)] @@ -118,6 +126,7 @@ pub struct WASMShape { impl SoftSkiaWASM { #[wasm_bindgen(constructor)] pub fn new(id: usize) -> Self { + utils::set_panic_hook(); let instance = Instance::new(id); SoftSkiaWASM(instance) } @@ -206,7 +215,6 @@ impl SoftSkiaWASM { #[wasm_bindgen(js_name = setAttrBySerde)] pub fn set_attr_by_serde(&mut self, id: usize, value: JsValue) { let message: WASMShape = serde_wasm_bindgen::from_value(value).unwrap(); - match message.attr { WASMShapesAttr::R(WASMRectAttr{ width, height, x, y , color, style}) => { let color = parse_color(color); @@ -281,6 +289,17 @@ impl SoftSkiaWASM { }) => { self.0.set_shape_to_child(id, Shapes::I(Image { image, x, y, width, height })) } + WASMShapesAttr::T(WASMTextAttr{ + x, + y, + text, + font_size, + color + }) => { + let color = parse_color(color); + let font_size = font_size.unwrap_or(16.0); + self.0.set_shape_to_child(id, Shapes::T(Text { text, x, y, font_size, color })) + } }; } } diff --git a/soft-skia/Cargo.toml b/soft-skia/Cargo.toml index 9d99f0e..8c36ca0 100644 --- a/soft-skia/Cargo.toml +++ b/soft-skia/Cargo.toml @@ -9,5 +9,6 @@ license = "MIT" [dependencies] png = "0.17.5" -tiny-skia = "0.9.0" -base64 = "0.21.0" \ No newline at end of file +tiny-skia = "0.10.0" +base64 = "0.21.0" +fontdue = "0.7.3" \ No newline at end of file diff --git a/soft-skia/assets/Roboto-Regular.ttf b/soft-skia/assets/Roboto-Regular.ttf new file mode 100644 index 0000000..8c082c8 Binary files /dev/null and b/soft-skia/assets/Roboto-Regular.ttf differ diff --git a/soft-skia/src/shape.rs b/soft-skia/src/shape.rs index a063625..8ffe05e 100644 --- a/soft-skia/src/shape.rs +++ b/soft-skia/src/shape.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; +use fontdue::{layout::{Layout, LayoutSettings, CoordinateSystem, TextStyle}, Font, Metrics}; pub use tiny_skia::{ColorU8, FillRule, Mask, Paint, PathBuilder, Pixmap, Stroke, Transform}; use tiny_skia::{LineCap, LineJoin, Path, PixmapPaint}; +use std::iter::zip; #[derive(Debug)] pub enum Shapes { @@ -11,6 +13,7 @@ pub enum Shapes { L(Line), P(Points), I(Image), + T(Text), } #[derive(Debug)] @@ -41,7 +44,10 @@ impl DrawContext { pub trait Shape { fn default() -> Self; fn draw(&self, pixmap: &mut Pixmap, context: &DrawContext) -> (); - fn get_path(&self, context: &DrawContext) -> Path; + fn get_path(&self, context: &DrawContext) -> Path { + let pb = PathBuilder::new(); + pb.finish().unwrap() + } } #[derive(Debug, Clone, Copy)] @@ -105,6 +111,15 @@ pub struct Image { pub height: u32, } +#[derive(Debug)] +pub struct Text { + pub text: String, + pub x: i32, + pub y: i32, + pub font_size: f32, + pub color: Option, +} + impl Shapes { pub fn draw(&self, pixmap: &mut Pixmap, context: &DrawContext) -> () { match self { @@ -114,6 +129,7 @@ impl Shapes { Shapes::L(line) => line.draw(pixmap, context), Shapes::P(points) => points.draw(pixmap, context), Shapes::I(image) => image.draw(pixmap, context), + Shapes::T(text) => text.draw(pixmap, context), } } @@ -125,6 +141,7 @@ impl Shapes { Shapes::L(line) => line.get_path(context), Shapes::P(points) => points.get_path(context), Shapes::I(image) => image.get_path(context), + Shapes::T(text) => text.get_path(context), } } } @@ -510,10 +527,79 @@ impl Shape for Image { None, ); } - fn get_path(&self, context: &DrawContext) -> Path { - let pb = PathBuilder::new(); - pb.finish().unwrap() + +} + +impl Shape for Text { + fn default() -> Self { + todo!() + } + + fn draw(&self, pixmap: &mut Pixmap, context: &DrawContext) -> () { + let font = include_bytes!("../assets/Roboto-Regular.ttf") as &[u8]; + let roboto_regular = Font::from_bytes(font, fontdue::FontSettings::default()).unwrap(); + let fonts = &[roboto_regular]; + let mut layout = Layout::new(CoordinateSystem::PositiveYDown); + layout.reset(&LayoutSettings { + ..LayoutSettings::default() + }); + layout.append(fonts, &TextStyle::new(&self.text, self.font_size, 0)); + + let mut glyphs:Vec> = vec![]; + self.text.chars().for_each(|c| { + let (_, bitmap) = fonts[0].rasterize(c, self.font_size); + glyphs.push(bitmap); + }); + let dim= compute_dim(&layout); + + let mut bitmap:Vec = vec![0; dim.0 * dim.1]; + for (pos, char_bitmap) in zip(layout.glyphs(), &glyphs) { + let x = pos.x as i32; + let y = pos.y as i32 as i32; + let width = pos.width as usize; + let height = pos.height as usize; + let mut i = 0; + for y in y..y+height as i32 { + for x in x..x+width as i32 { + let index = ((y * dim.0 as i32 + x)) as usize; + if index < bitmap.len() { + bitmap[index] = char_bitmap[i]; + } + i += 1; + } + } + } + let mut rgba_bitmap:Vec = vec![]; + for i in 0..bitmap.len() { + if let Some(color) = self.color { + rgba_bitmap.extend([color.red(), color.green(), color.blue(), bitmap[i]].iter()); + } else { + rgba_bitmap.extend([0, 0, 0, bitmap[i]].iter()); + } + } + + let p = Pixmap::from_vec(rgba_bitmap, tiny_skia::IntSize::from_wh(dim.0 as u32, dim.1 as u32).unwrap()).unwrap(); + pixmap.draw_pixmap( + self.x, + self.y, + p.as_ref(), + &PixmapPaint::default(), + Transform::from_row(1.0, 0.0, 0.0, 1.0, 0.0, 0.0), + None, + ); + + } +} + +fn compute_dim(layout: &Layout) -> (usize, usize) { + let (mut x1, mut y1, mut x2, mut y2): (i32, i32, i32, i32) = (0, 0, 0, 0); + for pos in layout.glyphs() { + x1 = x1.min(pos.x as i32); + y1 = y1.min(pos.y as i32); + x2 = x2.max(pos.x as i32+pos.width as i32); + y2 = y2.max(pos.y as i32+pos.height as i32); } + return (1+(x2-x1) as usize, (y2-y1) as usize) } #[cfg(test)] diff --git a/vue-playground/src/App.vue b/vue-playground/src/App.vue index 7ff732e..9fe3f5c 100644 --- a/vue-playground/src/App.vue +++ b/vue-playground/src/App.vue @@ -29,6 +29,7 @@ VLine, VPoints, VImage, + VText, }" @error="(e: any) => void 0" @input="input" @@ -98,6 +99,7 @@ import launch, { VLine, VPoints, VImage, + VText, } from "vue-skia"; import { VueLive } from "vue-live"; import GithubCorners from "@uivjs/vue-github-corners"; @@ -119,6 +121,7 @@ export default defineComponent({ VLine, VPoints, VImage, + VText }, data() { return { @@ -133,6 +136,7 @@ export default defineComponent({ VLine, VPoints, VImage, + VText, code, LoadingCode, debug: false, diff --git a/vue-playground/src/code.ts b/vue-playground/src/code.ts index 28e5cf2..ca02782 100644 --- a/vue-playground/src/code.ts +++ b/vue-playground/src/code.ts @@ -28,6 +28,7 @@ export default ` + `; diff --git a/vue-skia-framework/components/VText.vue b/vue-skia-framework/components/VText.vue new file mode 100644 index 0000000..03871ec --- /dev/null +++ b/vue-skia-framework/components/VText.vue @@ -0,0 +1,33 @@ + + + diff --git a/vue-skia-framework/main.js b/vue-skia-framework/main.js index 40bf0ec..e2a129e 100644 --- a/vue-skia-framework/main.js +++ b/vue-skia-framework/main.js @@ -8,7 +8,8 @@ import VRoundRect from "./components/VRoundRect.vue"; import VLine from "./components/VLine.vue"; import VPoints from "./components/VPoints.vue"; import VImage from './components/VImage.vue'; +import VText from './components/VText.vue'; export default launch; export { plugin as VueSkia } -export { VSurface, VGroup, VRect, VCircle, VRoundRect, VLine, VPoints, VImage } \ No newline at end of file +export { VSurface, VGroup, VRect, VCircle, VRoundRect, VLine, VPoints, VImage, VText } \ No newline at end of file diff --git a/vue-skia-framework/plugin/index.ts b/vue-skia-framework/plugin/index.ts index 719b29d..e5923ef 100644 --- a/vue-skia-framework/plugin/index.ts +++ b/vue-skia-framework/plugin/index.ts @@ -23,7 +23,8 @@ const WidgetList = [ 'RoundRect', 'Rect', 'Circle', - 'Image' + 'Image', + 'Text' ]; const VSKNode = (name: string) => { @@ -173,6 +174,19 @@ const VSKNode = (name: string) => { }, }); } + if (name === "Text") { + core.setAttrBySerde(instance._ssw_id, { + attr: { + T: { + text: attrs.text, + x: attrs.x, + y: attrs.y, + font_size: attrs.fontSize, + color: attrs.color, + }, + }, + }); + } } onBeforeUnmount(() => {