mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2301 from dotCooCoo/feat/glass-load-texture
Effects: Glass loads its texture from an image file
This commit is contained in:
Generated
+1
@@ -4634,6 +4634,7 @@ dependencies = [
|
||||
name = "vectorcraft-effects"
|
||||
version = "0.11.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
"serde_json",
|
||||
"vectorcraft-color",
|
||||
|
||||
@@ -15,6 +15,7 @@ vectorcraft-doc = { workspace = true }
|
||||
vectorcraft-pathops = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
vectorcraft-plugins = { workspace = true }
|
||||
image = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
|
||||
@@ -415,7 +415,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"distort.glass",
|
||||
"Glass…",
|
||||
DISTORT,
|
||||
"{distortion: 0..20 (5), smoothness: 1..15 (3), texture: \"blocks\"|\"canvas\"|\"frosted\"|\"tinyLens\" (\"frosted\"; surfaces made in code), scaling: % 50..200 (100), invert: bool (false; turns the surface's heights over)} makes the object look as if seen through glass",
|
||||
"{distortion: 0..20 (5), smoothness: 1..15 (3), texture: \"blocks\"|\"canvas\"|\"frosted\"|\"tinyLens\"|\"loaded\" (\"frosted\"; \"loaded\" draws the image in textureImage and draws Frosted when there is none; the others are surfaces made in code), textureImage?: {path} | {name?, dataBase64} | string (Load Texture…: an image file that File › Place reads, within the renderer's pixel allowance, stored as its gray levels in a grayscale PNG in base64; a file sets texture to \"loaded\" unless texture is given, and the stored string, as effect.list gives it, is kept as it is), scaling: % 50..200 (100), invert: bool (false; turns the surface's heights over)} makes the object look as if seen through glass. A loaded texture lies one image pixel per point at 100 %, centered on the object's center and repeated; each pixel's luma is the surface's height, from 0 for black to 1 pt for white, and a transparent pixel counts as middle gray",
|
||||
json!({"distortion": 5.0, "smoothness": 3.0, "texture": "frosted", "scaling": 100.0, "invert": false}),
|
||||
),
|
||||
r(
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
//! white grain. Every surface lies in document space around the object's centre, as the Texture
|
||||
//! filters' do.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
use super::loaded::{LOADED, LoadedTexture, texture};
|
||||
use super::pixelate::{Place, value_noise};
|
||||
use super::texture::{Texture, cell_noise, luma, premultiply, smooth_noise, straight};
|
||||
use super::{PixelSpace, blur_plane, pick};
|
||||
@@ -20,13 +23,19 @@ const SALT_RIPPLE: u64 = 136;
|
||||
/// Glass's surfaces, in its Texture menu's order: (label, parameter value).
|
||||
pub const GLASS_TEXTURES: [(&str, &str); 4] = [("Blocks", "blocks"), ("Canvas", "canvas"), ("Frosted", "frosted"), ("Tiny Lens", "tinyLens")];
|
||||
|
||||
/// Glass's Texture menu once a texture is loaded: [`GLASS_TEXTURES`], then the loaded one.
|
||||
pub const GLASS_TEXTURES_LOADED: [(&str, &str); 5] =
|
||||
[("Blocks", "blocks"), ("Canvas", "canvas"), ("Frosted", "frosted"), ("Tiny Lens", "tinyLens"), ("Loaded Texture", LOADED)];
|
||||
|
||||
/// A Glass surface.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum GlassTexture {
|
||||
Blocks,
|
||||
Canvas,
|
||||
Frosted,
|
||||
TinyLens,
|
||||
/// An image the user loaded ([`LoadedTexture`]).
|
||||
Loaded(Arc<LoadedTexture>),
|
||||
}
|
||||
|
||||
impl GlassTexture {
|
||||
@@ -37,13 +46,23 @@ impl GlassTexture {
|
||||
pick(&GLASS_TEXTURES, &Self::ALL, value, GlassTexture::Frosted)
|
||||
}
|
||||
|
||||
/// The surface of `value` with `stored` the effect's `textureImage`: the loaded texture
|
||||
/// for [`LOADED`] (any case) when `stored` holds one, else as [`GlassTexture::parse`].
|
||||
pub fn parse_with(value: &str, stored: Option<&str>) -> Self {
|
||||
match stored.filter(|_| value.eq_ignore_ascii_case(LOADED)).and_then(texture) {
|
||||
Some(t) => GlassTexture::Loaded(t),
|
||||
None => Self::parse(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// The surface's height at `p` (points at 100 %), in points: slopes of about 1 at most.
|
||||
fn height(self, p: Point) -> f64 {
|
||||
fn height(&self, p: Point) -> f64 {
|
||||
match self {
|
||||
GlassTexture::Blocks => blocks(p),
|
||||
GlassTexture::Canvas => Texture::Canvas.height(p),
|
||||
GlassTexture::Frosted => frosted(p),
|
||||
GlassTexture::TinyLens => tiny_lens(p),
|
||||
GlassTexture::Loaded(t) => t.height(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +122,7 @@ pub(super) fn glass(
|
||||
space: &PixelSpace,
|
||||
distortion: f64,
|
||||
smoothness: f64,
|
||||
texture: GlassTexture,
|
||||
texture: &GlassTexture,
|
||||
scaling: f64,
|
||||
invert: bool,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Load Texture (Effect › Distort › Glass): a surface made from an image the user loads.
|
||||
//!
|
||||
//! The effect keeps the image's gray levels in its parameters ([`TEXTURE_IMAGE`]: a grayscale PNG,
|
||||
//! base64), so the document saves the texture with the effect, and copies of the effect carry it.
|
||||
//! Each pixel's gray is the surface's height there: white stands [`RELIEF`] above black. The image
|
||||
//! lies one pixel per point at 100 %, centered on the object's center and repeated in both
|
||||
//! directions.
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use super::texture::luma;
|
||||
|
||||
/// The effect parameter that holds a loaded texture: its gray levels as a grayscale PNG, base64.
|
||||
pub const TEXTURE_IMAGE: &str = "textureImage";
|
||||
|
||||
/// The Texture menu's value for the loaded texture.
|
||||
pub const LOADED: &str = "loaded";
|
||||
|
||||
/// How far (points) a loaded texture's white stands above its black: a black pixel beside a white
|
||||
/// one gives a slope of about 1, the most the built-in surfaces have.
|
||||
const RELIEF: f64 = 1.0;
|
||||
|
||||
/// How many decoded textures [`texture`] keeps.
|
||||
const KEEP: usize = 4;
|
||||
|
||||
/// A loaded texture: `width` × `height` gray levels, row by row from the top.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct LoadedTexture {
|
||||
width: u32,
|
||||
height: u32,
|
||||
gray: Vec<u8>,
|
||||
}
|
||||
|
||||
impl LoadedTexture {
|
||||
/// The texture of `width` × `height` straight (not premultiplied) RGBA pixels: each pixel's
|
||||
/// luma over middle gray, as much as its alpha covers it (a transparent pixel is middle gray).
|
||||
/// `None` when the image is empty or `rgba` doesn't hold exactly that many pixels.
|
||||
pub fn from_rgba(width: u32, height: u32, rgba: &[u8]) -> Option<Self> {
|
||||
let n = (width as usize).checked_mul(height as usize).filter(|n| *n > 0)?;
|
||||
let (px, rest) = rgba.as_chunks::<4>();
|
||||
if px.len() != n || !rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let gray = px
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let a = f32::from(p[3]) / 255.0;
|
||||
let l = luma([p[0], p[1], p[2]].map(|v| f32::from(v) / 255.0));
|
||||
((a * l + (1.0 - a) * 0.5) * 255.0).round().clamp(0.0, 255.0) as u8
|
||||
})
|
||||
.collect();
|
||||
Some(Self { width, height, gray })
|
||||
}
|
||||
|
||||
/// The size in pixels.
|
||||
pub fn size(&self) -> (u32, u32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Its [`TEXTURE_IMAGE`] value: the gray levels as a grayscale PNG, base64; `None` when the
|
||||
/// PNG can't be written.
|
||||
pub fn to_stored(&self) -> Option<String> {
|
||||
let img = image::GrayImage::from_raw(self.width, self.height, self.gray.clone())?;
|
||||
let mut png = Vec::new();
|
||||
img.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).ok()?;
|
||||
Some(base64_encode(&png))
|
||||
}
|
||||
|
||||
/// The texture of a PNG's gray levels, decoded within [`vectorcraft_doc::MAX_RASTER_ALLOC`];
|
||||
/// `None` when `png` isn't a PNG or holds no pixels.
|
||||
fn from_png(png: &[u8]) -> Option<Self> {
|
||||
let mut reader = image::ImageReader::with_format(Cursor::new(png), image::ImageFormat::Png);
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(vectorcraft_doc::MAX_RASTER_ALLOC);
|
||||
reader.limits(limits);
|
||||
let img = reader.decode().ok()?.to_luma8();
|
||||
let (width, height) = img.dimensions();
|
||||
(width > 0 && height > 0).then(|| Self { width, height, gray: img.into_raw() })
|
||||
}
|
||||
|
||||
/// The surface's height (points) at `p` (points at 100 %, from the object's center): the gray
|
||||
/// levels around it interpolated, 0 for black and [`RELIEF`] for white.
|
||||
pub(super) fn height(&self, p: Point) -> f64 {
|
||||
let (w, h) = (f64::from(self.width), f64::from(self.height));
|
||||
// Pixel centers lie at whole coordinates, and the image's center lies at the origin.
|
||||
let (x, y) = (p.x + 0.5 * w - 0.5, p.y + 0.5 * h - 0.5);
|
||||
let (x0, y0) = (x.floor(), y.floor());
|
||||
let (fx, fy) = (x - x0, y - y0);
|
||||
let at = |i: f64, j: f64| {
|
||||
let (i, j) = (i.rem_euclid(w) as usize, j.rem_euclid(h) as usize);
|
||||
self.gray.get(j * self.width as usize + i).map_or(0.5, |g| f64::from(*g) / 255.0)
|
||||
};
|
||||
let top = at(x0, y0) + (at(x0 + 1.0, y0) - at(x0, y0)) * fx;
|
||||
let bottom = at(x0, y0 + 1.0) + (at(x0 + 1.0, y0 + 1.0) - at(x0, y0 + 1.0)) * fx;
|
||||
RELIEF * (top + (bottom - top) * fy)
|
||||
}
|
||||
}
|
||||
|
||||
/// The textures [`texture`] decoded last, with their stored form, the newest last.
|
||||
static DECODED: Mutex<Vec<(String, Arc<LoadedTexture>)>> = Mutex::new(Vec::new());
|
||||
|
||||
/// The texture a [`TEXTURE_IMAGE`] value holds; `None` when it isn't base64 of a PNG. The last
|
||||
/// [`KEEP`] textures decoded are kept for the next call.
|
||||
pub fn texture(stored: &str) -> Option<Arc<LoadedTexture>> {
|
||||
let mut decoded = DECODED.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if let Some(i) = decoded.iter().position(|(s, _)| s == stored) {
|
||||
let entry = decoded.remove(i);
|
||||
let t = entry.1.clone();
|
||||
decoded.push(entry);
|
||||
return Some(t);
|
||||
}
|
||||
let t = Arc::new(LoadedTexture::from_png(&base64_decode(stored)?)?);
|
||||
if decoded.len() >= KEEP {
|
||||
decoded.remove(0);
|
||||
}
|
||||
decoded.push((stored.to_string(), t.clone()));
|
||||
Some(t)
|
||||
}
|
||||
|
||||
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/// Standard base64 (RFC 4648, with padding), written as `vectorcraft_format::base64_encode`
|
||||
/// writes it.
|
||||
fn base64_encode(data: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
|
||||
for c in data.chunks(3) {
|
||||
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
|
||||
out.push(B64[(n >> 18) as usize & 63] as char);
|
||||
out.push(B64[(n >> 12) as usize & 63] as char);
|
||||
out.push(if c.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
|
||||
out.push(if c.len() > 2 { B64[n as usize & 63] as char } else { '=' });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Standard base64 (RFC 4648, with padding; whitespace skipped), read as
|
||||
/// `vectorcraft_format::base64_decode` reads it; `None` when it isn't base64.
|
||||
fn base64_decode(s: &str) -> Option<Vec<u8>> {
|
||||
let val = |c: u8| B64.iter().position(|&b| b == c).map(|p| p as u32);
|
||||
let clean: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
|
||||
if !clean.len().is_multiple_of(4) {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::with_capacity(clean.len() / 4 * 3);
|
||||
for c in clean.chunks(4) {
|
||||
let mut n = 0u32;
|
||||
let mut pad = 0;
|
||||
for &b in c {
|
||||
n <<= 6;
|
||||
if b == b'=' {
|
||||
pad += 1;
|
||||
} else {
|
||||
n |= val(b)?;
|
||||
}
|
||||
}
|
||||
out.push((n >> 16) as u8);
|
||||
if pad < 2 {
|
||||
out.push((n >> 8) as u8);
|
||||
}
|
||||
if pad < 1 {
|
||||
out.push(n as u8);
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod blur;
|
||||
mod brushstrokes;
|
||||
mod distort;
|
||||
mod edges;
|
||||
mod loaded;
|
||||
mod pixelate;
|
||||
mod sharpen;
|
||||
mod texture;
|
||||
@@ -29,7 +30,8 @@ use crate::util::{flag, num, text};
|
||||
|
||||
pub use blur::{SMART_BLUR_MODES, SmartBlurMode};
|
||||
pub use brushstrokes::{STROKE_DIRECTIONS, StrokeDirection};
|
||||
pub use distort::{GLASS_TEXTURES, GlassTexture};
|
||||
pub use distort::{GLASS_TEXTURES, GLASS_TEXTURES_LOADED, GlassTexture};
|
||||
pub use loaded::{LOADED, LoadedTexture, TEXTURE_IMAGE, texture};
|
||||
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
|
||||
pub use texture::{GRAIN_TYPES, Grain, LIGHT_DIRECTIONS, Light, TEXTURES, Texture};
|
||||
|
||||
@@ -67,7 +69,7 @@ pub const PIXEL_EFFECTS: [&str; 28] = [
|
||||
|
||||
/// A Photoshop-style raster effect with its parameters read and clamped to their ranges. Lengths
|
||||
/// are in document units.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PixelFx {
|
||||
/// Artistic › Film Grain: monochrome grain in shadows and midtones, a wider bright
|
||||
/// highlight area (0..20), and tonal intensity (0..10), preserving coverage.
|
||||
@@ -111,9 +113,9 @@ pub enum PixelFx {
|
||||
/// Distort › Diffuse Glow: the highlights glow white as strongly as `glow` (0..20), from the
|
||||
/// brightness `clear` (0..20) leaves clear, under white grain as dense as `graininess` (0..10).
|
||||
DiffuseGlow { graininess: f64, glow: f64, clear: f64 },
|
||||
/// Distort › Glass: the object seen through a `texture` surface scaled by `scaling` (0.5..2),
|
||||
/// bent by `distortion` (0..20), smoothed by `smoothness` (1..15), its heights turned over when
|
||||
/// `invert`.
|
||||
/// Distort › Glass: the object seen through a `texture` surface (a built-in one, or an image
|
||||
/// the user loaded) scaled by `scaling` (0.5..2), bent by `distortion` (0..20), smoothed by
|
||||
/// `smoothness` (1..15), its heights turned over when `invert`.
|
||||
Glass { distortion: f64, smoothness: f64, texture: GlassTexture, scaling: f64, invert: bool },
|
||||
/// Distort › Ocean Ripple: the object under ripples of `size` (1..15) shifting it as much as
|
||||
/// `magnitude` (0..20) says.
|
||||
@@ -263,7 +265,7 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
|
||||
"distort.glass" => PixelFx::Glass {
|
||||
distortion: num(p, "distortion", 5.0).clamp(0.0, 20.0),
|
||||
smoothness: num(p, "smoothness", 3.0).clamp(1.0, 15.0),
|
||||
texture: GlassTexture::parse(text(p, "texture", "frosted")),
|
||||
texture: GlassTexture::parse_with(text(p, "texture", "frosted"), p.get(TEXTURE_IMAGE).and_then(Value::as_str)),
|
||||
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
|
||||
invert: flag(p, "invert", false),
|
||||
},
|
||||
@@ -432,7 +434,7 @@ impl PixelFx {
|
||||
PixelFx::SprayedStrokes { length, radius, direction } => brushstrokes::sprayed_strokes(px, w, h, space, length, radius, direction),
|
||||
PixelFx::SumiE { width, pressure, contrast } => brushstrokes::sumi_e(px, w, h, space, width, pressure, contrast),
|
||||
PixelFx::DiffuseGlow { graininess, glow, clear } => distort::diffuse_glow(px, w, h, space, graininess, glow, clear),
|
||||
PixelFx::Glass { distortion, smoothness, texture, scaling, invert } => {
|
||||
PixelFx::Glass { distortion, smoothness, ref texture, scaling, invert } => {
|
||||
distort::glass(px, w, h, space, distortion, smoothness, texture, scaling, invert)
|
||||
}
|
||||
PixelFx::OceanRipple { size, magnitude } => distort::ocean_ripple(px, w, h, space, size, magnitude),
|
||||
@@ -458,8 +460,8 @@ impl PixelFx {
|
||||
|
||||
/// The entry of `all` whose value in `table` (the same order) is `value`, any case; `default`
|
||||
/// otherwise.
|
||||
fn pick<T: Copy>(table: &[(&str, &str)], all: &[T], value: &str, default: T) -> T {
|
||||
table.iter().zip(all).find(|((_, v), _)| v.eq_ignore_ascii_case(value)).map_or(default, |(_, t)| *t)
|
||||
fn pick<T: Clone>(table: &[(&str, &str)], all: &[T], value: &str, default: T) -> T {
|
||||
table.iter().zip(all).find(|((_, v), _)| v.eq_ignore_ascii_case(value)).map_or(default, |(_, t)| t.clone())
|
||||
}
|
||||
|
||||
/// `data` as `w` × `h` RGBA pixels, `None` when it isn't that size (or is empty).
|
||||
|
||||
@@ -36,7 +36,7 @@ fn at(d: &[u8], w: usize, x: usize, y: usize) -> [u8; 4] {
|
||||
fn fx(id: &str, p: serde_json::Value) -> PixelFx {
|
||||
let e = Effect { id: id.into(), params: p, visible: true };
|
||||
match raster_effects(&[e]).first() {
|
||||
Some(RasterFx::Pixel(p)) => *p,
|
||||
Some(RasterFx::Pixel(p)) => p.clone(),
|
||||
other => panic!("{id}: {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -1419,6 +1419,68 @@ fn glass_bends_the_image_by_its_texture() {
|
||||
assert_ne!(run(json!({"distortion": 12, "smoothness": 15})), run(json!({"distortion": 12, "smoothness": 1})));
|
||||
}
|
||||
|
||||
/// The stored form of a `w` × `h` texture whose straight RGBA pixels `f` gives.
|
||||
fn stored(w: usize, h: usize, f: impl Fn(usize, usize) -> [u8; 4]) -> String {
|
||||
LoadedTexture::from_rgba(w as u32, h as u32, &image(w, h, f)).and_then(|t| t.to_stored()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_loaded_texture_is_its_gray_over_middle_gray() {
|
||||
let t = stored(3, 1, |x, _| [[255, 255, 255, 255], [0, 0, 0, 0], [255, 0, 0, 255]][x]);
|
||||
let loaded = texture(&t).unwrap();
|
||||
assert_eq!(loaded.size(), (3, 1));
|
||||
assert_eq!(texture(&t), Some(loaded.clone()), "read again from the cache");
|
||||
// A white pixel is 1 high, a transparent one is middle gray and a red one is its luma. Heights
|
||||
// between pixel centers are interpolated, and the image repeats: x = -1 is the first pixel's
|
||||
// center, and x = 2 is the same pixel again.
|
||||
let h = |x: f64| loaded.height(Point::new(x, 0.0));
|
||||
assert_eq!(h(-1.0), 1.0);
|
||||
assert_eq!(h(0.0), 128.0 / 255.0);
|
||||
assert_eq!(h(1.0), 76.0 / 255.0);
|
||||
assert_eq!(h(2.0), 1.0);
|
||||
assert_eq!(h(-0.5), 0.5 * (1.0 + 128.0 / 255.0));
|
||||
assert!(LoadedTexture::from_rgba(0, 4, &[]).is_none() && LoadedTexture::from_rgba(2, 2, &[0; 12]).is_none());
|
||||
assert!(texture("not base64!").is_none() && texture("aGVsbG8=").is_none(), "base64 of a PNG only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glass_bends_the_image_by_a_loaded_texture() {
|
||||
let (w, h) = (48, 40);
|
||||
let src = stripes(w, h);
|
||||
let run = |p: serde_json::Value| {
|
||||
let mut d = src.clone();
|
||||
fx("distort.glass", p).apply(&mut d, w, h, &space(w, h));
|
||||
d
|
||||
};
|
||||
// Rings around the texture's center, 9 × 7 pixels, repeated.
|
||||
let rings = stored(9, 7, |x, y| grey((((x as f64 - 4.0).hypot(y as f64 - 3.0) * 60.0) as u32 % 256) as u8));
|
||||
let loaded = json!({"texture": "loaded", "textureImage": rings, "distortion": 12});
|
||||
let bent = run(loaded.clone());
|
||||
assert_ne!(bent, src);
|
||||
for (_, texture) in GLASS_TEXTURES {
|
||||
assert_ne!(bent, run(json!({"texture": texture, "distortion": 12})), "differs from {texture}");
|
||||
}
|
||||
assert_ne!(run(json!({"texture": "loaded", "textureImage": rings, "distortion": 12, "invert": true})), bent, "inverted");
|
||||
assert_ne!(run(json!({"texture": "loaded", "textureImage": rings, "distortion": 12, "scaling": 200})), bent, "scaled");
|
||||
for (x, y) in (6..w - 6).flat_map(|x| (6..h - 6).map(move |y| (x, y))) {
|
||||
let p = at(&bent, w, x, y);
|
||||
assert!(p[3] == 255 && p[0] >= 20 && p[0] <= 230, "at ({x}, {y}): {p:?}");
|
||||
}
|
||||
assert_eq!(run(json!({"texture": "loaded", "textureImage": rings, "distortion": 0})), src, "no distortion, no change");
|
||||
// A flat texture bends nothing, whatever its gray; so does a transparent one (middle gray).
|
||||
for flat in [grey(0), grey(200), [0, 0, 0, 0]] {
|
||||
assert_eq!(run(json!({"texture": "loaded", "textureImage": stored(5, 5, |_, _| flat), "distortion": 20})), src, "{flat:?}");
|
||||
}
|
||||
// The image is kept but not used with a built-in texture; without one, "loaded" is Frosted.
|
||||
assert_eq!(run(json!({"texture": "blocks", "textureImage": rings, "distortion": 12})), run(json!({"texture": "blocks", "distortion": 12})));
|
||||
assert_eq!(fx("distort.glass", json!({"texture": "loaded"})), fx("distort.glass", json!({})));
|
||||
assert_eq!(fx("distort.glass", json!({"texture": "LOADED", "textureImage": "not a texture"})), fx("distort.glass", json!({})));
|
||||
assert_eq!(
|
||||
fx("distort.glass", loaded),
|
||||
PixelFx::Glass { distortion: 12.0, smoothness: 3.0, texture: GlassTexture::Loaded(texture(&rings).unwrap()), scaling: 1.0, invert: false }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocean_ripple_moves_content_no_farther_than_its_magnitude() {
|
||||
let (w, h) = (64, 32);
|
||||
|
||||
@@ -142,6 +142,9 @@ pub(crate) fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if id == effects::CROP_MARKS && params.get("style").is_none() {
|
||||
params["style"] = json!(s.crop_mark_style().id());
|
||||
}
|
||||
if id == GLASS {
|
||||
load_texture(&mut params, C)?;
|
||||
}
|
||||
let effect = effects::new_effect(id, ¶ms).ok_or_else(|| bad(C, format!("unknown effect `{id}`")))?;
|
||||
let label = effects::effect_info(id).map(|e| e.label.trim_end_matches('…').to_string()).unwrap_or_default();
|
||||
if let Some(loose) = loose_for_pathfinder(s, p, id)? {
|
||||
@@ -297,7 +300,7 @@ pub(crate) fn set_params_effects(s: &Session, p: &Value) -> Vec<String> {
|
||||
fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "effect.setParams";
|
||||
let index = index_param(p, "index", C)?;
|
||||
let params = p.get("params").cloned().unwrap_or(Value::Null);
|
||||
let mut params = p.get("params").cloned().unwrap_or(Value::Null);
|
||||
if !params.is_null() && !params.is_object() {
|
||||
return Err(bad(C, "`params` must be an object"));
|
||||
}
|
||||
@@ -310,6 +313,9 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.transpose()?;
|
||||
let visible = p.get("visible").and_then(Value::as_bool);
|
||||
let item = item_target(s, p, C)?;
|
||||
if set_params_effects(s, p).iter().any(|e| e == GLASS) {
|
||||
load_texture(&mut params, C)?;
|
||||
}
|
||||
for id in appearance_targets(s, p)? {
|
||||
let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;
|
||||
let target = item.effects_item(&n.appearance, C)?;
|
||||
@@ -356,6 +362,31 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "ids": ids_json(&ids) }))
|
||||
}
|
||||
|
||||
// ---------- Load Texture ----------
|
||||
|
||||
/// Effect › Distort › Glass.
|
||||
const GLASS: &str = "distort.glass";
|
||||
|
||||
/// Glass's `textureImage` in `params` as Glass stores it: an image file (`{path}`, or `{name?,
|
||||
/// dataBase64}`) is read with [`fileio::texture_image`] and picked (`texture: "loaded"`) unless
|
||||
/// `params` give a `texture`; a string must be a texture as Glass stores it.
|
||||
fn load_texture(params: &mut Value, cmd: &str) -> Result<()> {
|
||||
let pick = params.get("texture").is_none();
|
||||
let Some(v) = params.get_mut(effects::pixel::TEXTURE_IMAGE) else { return Ok(()) };
|
||||
match v {
|
||||
Value::String(s) if effects::pixel::texture(s).is_some() => {}
|
||||
Value::Object(_) => {
|
||||
let bytes = fileio::source(v, cmd)?.bytes;
|
||||
*v = json!(fileio::texture_image(&bytes).map_err(|e| bad(cmd, format!("textureImage: {e}")))?);
|
||||
if pick {
|
||||
params["texture"] = json!(effects::pixel::LOADED);
|
||||
}
|
||||
}
|
||||
_ => return Err(bad(cmd, "textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------- Expand Appearance ----------
|
||||
|
||||
/// Has `n` an appearance Expand Appearance turns into objects: a path that isn't basic (several
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::io::Cursor;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
|
||||
use vectorcraft_geom::Affine;
|
||||
use vectorcraft_render::effects::pixel::LoadedTexture;
|
||||
|
||||
use super::super::*;
|
||||
use super::pdfimport::LoadOptions;
|
||||
@@ -491,6 +492,43 @@ fn as_png(img: image::RgbaImage, ppi: Option<(f64, f64)>) -> Result<RasterImage>
|
||||
stored(png, "image/png", size, ppi)
|
||||
}
|
||||
|
||||
/// Effect › Distort › Glass › Load Texture…: encoded image `bytes`, in any format [`raster_image`]
|
||||
/// reads, as the `textureImage` Glass stores ([`LoadedTexture::to_stored`]). The pixels are decoded
|
||||
/// within [`MAX_RASTER_ALLOC`], counted as RGBA, as the renderer decodes them (#1717); a CMYK image
|
||||
/// is converted with the active color settings.
|
||||
pub fn texture_image(bytes: &[u8]) -> Result<String> {
|
||||
if let Some(note) = too_large_note(bytes) {
|
||||
return Err(err(note));
|
||||
}
|
||||
let RasterImage { blob, width, height, .. } = raster_image(bytes)?;
|
||||
// The allowance counts the pixels as RGBA: a gray image takes four times its file's samples (#1717).
|
||||
if u64::from(width) * u64::from(height) * 4 > MAX_RASTER_ALLOC {
|
||||
return Err(err(format!("an image of {width} × {height} pixels is too large to draw")));
|
||||
}
|
||||
let texture = match blob.cmyk() {
|
||||
Some(inks) => {
|
||||
let cms = vectorcraft_color::cms::active();
|
||||
let rgba: Vec<u8> = (inks.data.as_chunks::<4>().0.iter())
|
||||
.flat_map(|c| {
|
||||
let rgb = cms.cmyk_to_srgb(c.map(|v| f32::from(v) / 255.0), false);
|
||||
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
[r, g, b, 255]
|
||||
})
|
||||
.collect();
|
||||
LoadedTexture::from_rgba(inks.width, inks.height, &rgba)
|
||||
}
|
||||
None => {
|
||||
let mut reader = image::ImageReader::new(Cursor::new(blob.bytes.as_slice())).with_guessed_format().map_err(err)?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(MAX_RASTER_ALLOC);
|
||||
reader.limits(limits);
|
||||
let img = reader.decode().map_err(err)?.to_rgba8();
|
||||
LoadedTexture::from_rgba(img.width(), img.height(), img.as_raw())
|
||||
}
|
||||
};
|
||||
texture.and_then(|t| t.to_stored()).ok_or_else(|| err("the image can't be read as a texture"))
|
||||
}
|
||||
|
||||
/// An image's encoded `bytes`, `width` × `height` pixels, ready to embed.
|
||||
fn stored(bytes: Vec<u8>, mime: &'static str, (width, height): (u32, u32), ppi: Option<(f64, f64)>) -> Result<RasterImage> {
|
||||
if width == 0 || height == 0 {
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(crate) use load::native_file;
|
||||
pub(crate) use load::source;
|
||||
pub use load::{
|
||||
Loaded, RasterImage, ReadFile, detect, file_name, load, load_with, open_bytes, open_bytes_with, open_read, open_template, raster_image,
|
||||
read_for_open,
|
||||
read_for_open, texture_image,
|
||||
};
|
||||
pub use load::{losses_summary, overwrite_losses};
|
||||
pub(crate) use native::preview_png;
|
||||
|
||||
@@ -150,6 +150,94 @@ fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A PNG of rings around its center, 12 × 10 pixels.
|
||||
fn rings_png() -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_fn(12, 10, |x, y| {
|
||||
let v = ((f64::from(x) - 5.5).hypot(f64::from(y) - 4.5) * 50.0) as u32 % 256;
|
||||
image::Rgba([v as u8, v as u8, 255 - v as u8, 255])
|
||||
});
|
||||
let mut png = Vec::new();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
png
|
||||
}
|
||||
|
||||
/// Glass's parameters on the striped group.
|
||||
fn glass_params(s: &Session, g: u64) -> Value {
|
||||
s.doc().unwrap().doc.node(NodeId(g)).unwrap().appearance.effects[0].params.clone()
|
||||
}
|
||||
|
||||
/// Effect › Distort › Glass › Load Texture: an image file, given by path or as bytes, is stored
|
||||
/// with the effect as its gray levels and picked; the canvas draws it, and a save and open keeps it.
|
||||
#[test]
|
||||
fn glass_loads_a_texture_saves_it_and_draws_it() {
|
||||
let png = rings_png();
|
||||
let data = vectorcraft_format::base64_encode(&png);
|
||||
let (mut s, g) = striped();
|
||||
let file = json!({"name": "rings.png", "dataBase64": data});
|
||||
s.execute("effect.apply", &json!({"effect": "distort.glass", "ids": [g], "params": {"distortion": 12, "textureImage": file}})).unwrap();
|
||||
let p = glass_params(&s, g);
|
||||
assert_eq!(p["texture"], json!("loaded"), "loading a texture picks it");
|
||||
let stored = p["textureImage"].as_str().unwrap().to_string();
|
||||
let texture = vectorcraft_render::effects::pixel::texture(&stored).unwrap();
|
||||
assert_eq!(texture.size(), (12, 10));
|
||||
let loaded = render(&s);
|
||||
// The same Glass with each built-in texture draws differently.
|
||||
for (_, builtin) in vectorcraft_render::effects::pixel::GLASS_TEXTURES {
|
||||
s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": {"texture": builtin}})).unwrap();
|
||||
assert!(mean_diff(&loaded, &render(&s)) > 0.001, "{builtin}");
|
||||
}
|
||||
s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": {"texture": "loaded"}})).unwrap();
|
||||
assert_eq!(render(&s).pixels, loaded.pixels, "the texture stayed with the effect");
|
||||
// Saved and opened again: the same texture, drawn the same.
|
||||
let doc = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
assert_eq!(doc.node(NodeId(g)).unwrap().appearance.effects[0].params, glass_params(&s, g));
|
||||
assert_eq!(Renderer::new().render_region(&doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true).pixels, loaded.pixels);
|
||||
// The same file by path, through Effect Options, stores the same texture.
|
||||
let dir = std::env::temp_dir().join(format!("vc-glass-texture-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("rings.png");
|
||||
std::fs::write(&path, &png).unwrap();
|
||||
let (mut t, h) = striped();
|
||||
t.execute("effect.apply", &json!({"effect": "distort.glass", "ids": [h], "params": {"distortion": 12}})).unwrap();
|
||||
assert!(glass_params(&t, h).get("textureImage").is_none(), "a Glass without a loaded texture has no textureImage");
|
||||
t.execute("effect.setParams", &json!({"index": 0, "ids": [h], "params": {"textureImage": {"path": path.to_string_lossy()}}})).unwrap();
|
||||
assert_eq!(glass_params(&t, h)["textureImage"], json!(stored));
|
||||
assert_eq!(glass_params(&t, h)["texture"], json!("loaded"));
|
||||
assert_eq!(render(&t).pixels, loaded.pixels);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
// The stored string is taken as it is (as effect.list gives it, and a graphic style copies it).
|
||||
t.execute("effect.apply", &json!({"effect": "distort.glass", "ids": [h], "params": {"textureImage": stored, "texture": "frosted"}})).unwrap();
|
||||
let second = t.doc().unwrap().doc.node(NodeId(h)).unwrap().appearance.effects[1].params.clone();
|
||||
assert_eq!((&second["textureImage"], &second["texture"]), (&json!(stored), &json!("frosted")), "kept, not picked: texture was given");
|
||||
t.execute("edit.undo", &json!({})).unwrap();
|
||||
t.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(glass_params(&t, h).get("textureImage").is_none(), "undone");
|
||||
}
|
||||
|
||||
/// A texture that isn't an image file, or isn't one Glass stored, is refused and changes nothing.
|
||||
#[test]
|
||||
fn glass_refuses_a_texture_that_is_not_an_image() {
|
||||
let (mut s, g) = striped();
|
||||
let junk = vectorcraft_format::base64_encode(b"not an image");
|
||||
let not_stored = "textureImage: give {path} or {name, dataBase64}";
|
||||
let refused = [
|
||||
(json!({"name": "a.png", "dataBase64": junk}), "not an image VectorCraft reads"),
|
||||
(json!("bm90IGEgdGV4dHVyZQ=="), not_stored),
|
||||
(json!(5), not_stored),
|
||||
(json!({"path": "C:/no/such/texture.png"}), "texture.png"),
|
||||
];
|
||||
for (texture, says) in refused {
|
||||
let e = s.execute("effect.apply", &json!({"effect": "distort.glass", "ids": [g], "params": {"textureImage": texture}})).unwrap_err();
|
||||
assert!(e.to_string().contains(says), "{texture}: {e}");
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(g)).unwrap().appearance.effects.is_empty(), "{texture}");
|
||||
}
|
||||
s.execute("effect.apply", &json!({"effect": "distort.glass", "ids": [g]})).unwrap();
|
||||
let before = glass_params(&s, g);
|
||||
let e = s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": {"textureImage": {"dataBase64": junk}}})).unwrap_err();
|
||||
assert!(e.to_string().contains("textureImage"), "{e}");
|
||||
assert_eq!(glass_params(&s, g), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spin_blur_reaches_past_the_corners() {
|
||||
let (mut s, g) = striped();
|
||||
|
||||
@@ -149,6 +149,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let changed = match vectorcraft_plugins::effect::installed(&id) {
|
||||
Some(plugin) => form::schema_fields(ui, d, &plugin.manifest().params),
|
||||
None if vectorcraft_effects::is_adjustment(&id) => adjust_fields(ui, d),
|
||||
None if id == GLASS => glass_fields(app, ui, d),
|
||||
None if id == TRANSFORM => transform_fields(ui, d, app.session.general_unit()),
|
||||
None if id == ZIG_ZAG => zig_zag_fields(ui, d, app.session.general_unit()),
|
||||
None if id == DROP_SHADOW => drop_shadow_fields(ui, d, app.session.general_unit()),
|
||||
@@ -205,6 +206,56 @@ fn film_grain_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
before != d.fields
|
||||
}
|
||||
|
||||
/// The Glass effect (Effect › Distort › Glass…).
|
||||
const GLASS: &str = "distort.glass";
|
||||
|
||||
/// Glass's dialog: its parameters, the Texture menu ending with Loaded Texture once a texture is
|
||||
/// loaded, and Load Texture… with the loaded texture's size. Returns whether the values differ from
|
||||
/// those last previewed.
|
||||
fn glass_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
use vectorcraft_effects::pixel::{GLASS_TEXTURES, GLASS_TEXTURES_LOADED, TEXTURE_IMAGE, texture};
|
||||
// The stored texture is shown as its size, not as a text field.
|
||||
let stored = d.fields.remove(TEXTURE_IMAGE);
|
||||
let size = stored.as_ref().and_then(Value::as_str).and_then(texture).map(|t| t.size());
|
||||
let menu: form::Choices = if size.is_some() { &GLASS_TEXTURES_LOADED } else { &GLASS_TEXTURES };
|
||||
let doc = vectorcraft_effects::effect_info(GLASS).map(|e| e.params).unwrap_or_default();
|
||||
let unit = app.session.general_unit();
|
||||
let pick = |k: &str| if k == "texture" { Some(menu) } else { choices(GLASS, k) };
|
||||
form::param_fields(ui, d, &|k| vectorcraft_effects::is_length(GLASS, k, false), &pick, &|k| doc_rank(doc, k), unit);
|
||||
if let Some(v) = stored {
|
||||
d.fields.insert(TEXTURE_IMAGE.into(), v);
|
||||
}
|
||||
let mut load = false;
|
||||
ui.horizontal(|ui| {
|
||||
let can_read = app.services.read.is_some();
|
||||
load = ui.add_enabled_ui(can_read, |ui| widgets::flat_button(ui, tl!("Load Texture…"), 110.0)).inner.clicked();
|
||||
if let Some((w, h)) = size {
|
||||
widgets::dim_label(ui, &format!("{w} × {h} px"));
|
||||
}
|
||||
});
|
||||
if load
|
||||
&& let Err(e) = crate::picks::in_dialog(app, d, load_texture)
|
||||
&& e != "cancelled"
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
values_changed(d)
|
||||
}
|
||||
|
||||
/// Glass's Load Texture…: an image file the user picks, read into `textureImage` as Glass stores
|
||||
/// it and picked in the Texture menu.
|
||||
fn load_texture(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
|
||||
use vectorcraft_engine::cmd::fileio;
|
||||
let filters = fileio::FORMATS.iter().filter(|f| f.read && f.raster).map(|f| (f.label, f.extensions)).collect();
|
||||
let path = crate::picks::open(app, &crate::FilePick { filters, ..Default::default() }).ok_or("cancelled")?;
|
||||
// The button is off where files can't be read.
|
||||
let Some(read) = app.services.read.as_ref() else { return Ok(()) };
|
||||
let stored = fileio::texture_image(&read(&path)?).map_err(|e| e.to_string())?;
|
||||
d.fields.insert(vectorcraft_effects::pixel::TEXTURE_IMAGE.into(), json!(stored));
|
||||
d.fields.insert("texture".into(), json!(vectorcraft_effects::pixel::LOADED));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The parameters of the effect dialogs that pick one of some values, as (label, value).
|
||||
fn choices(effect: &str, key: &str) -> Option<form::Choices> {
|
||||
match (effect, key) {
|
||||
@@ -594,4 +645,39 @@ mod tests {
|
||||
assert_eq!(values.join("|"), listed, "{effect} {key}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Glass's Load Texture… reads the picked image into the dialog, the Texture menu shows Loaded
|
||||
/// Texture and the texture's size instead of its stored text, and OK applies it.
|
||||
#[test]
|
||||
fn glass_load_texture_reads_the_picked_image_into_the_effect() {
|
||||
let dir = std::env::temp_dir().join(format!("vectorcraft-glass-dialog-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("texture.png");
|
||||
image::RgbaImage::from_fn(6, 4, |x, y| image::Rgba([(x * 40) as u8, (y * 60) as u8, 90, 255])).save(&path).unwrap();
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
let picked = path.to_string_lossy().into_owned();
|
||||
app.services.pick_open = Some(Box::new(move |_: &crate::FilePick| Some(picked.clone())));
|
||||
app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string())));
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("effect.dialog", json!({"effect": GLASS})).unwrap();
|
||||
let paint = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, |app, ui| super::super::show(app, ui.ctx()));
|
||||
let text = paint(&mut app);
|
||||
assert!(text.lines().any(|l| l == "Load Texture…") && text.lines().any(|l| l == "Frosted"), "{text}");
|
||||
assert!(!text.contains("Loaded Texture") && !text.contains(" px"), "{text}");
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
load_texture(&mut app, &mut d).unwrap();
|
||||
let stored = d.str(vectorcraft_effects::pixel::TEXTURE_IMAGE);
|
||||
assert_eq!((d.str("texture"), vectorcraft_effects::pixel::texture(&stored).map(|t| t.size())), ("loaded".to_string(), Some((6, 4))));
|
||||
app.ui.dialog = Some(d);
|
||||
let text = paint(&mut app);
|
||||
assert!(text.lines().any(|l| l == "Loaded Texture") && text.lines().any(|l| l == "6 × 4 px"), "{text}");
|
||||
assert!(!text.contains(&stored[..24]), "the stored text isn't shown");
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
let st = app.session.doc().unwrap();
|
||||
let params = &st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params;
|
||||
assert_eq!((¶ms["texture"], ¶ms["textureImage"]), (&json!("loaded"), &json!(stored)));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4192,6 +4192,9 @@ effect Soft Меко
|
||||
Blocks Блокове
|
||||
Frosted Матирано
|
||||
Tiny Lens Малка леща
|
||||
Loaded Texture Заредена текстура
|
||||
Load Texture… Зареждане на текстура…
|
||||
Texture Image Изображение на текстурата
|
||||
Rename Data Set Преименуване на набор от данни
|
||||
Rename Variable Преименуване на променлива
|
||||
Variable Options Опции за променливи
|
||||
@@ -4762,6 +4765,7 @@ mark-style Japanese Японски
|
||||
theme Appearance Външен вид
|
||||
Load EPS into Clipboard Зареждане на EPS в клипборда
|
||||
@msg an image of {w} × {h} pixels is too large to draw изображение с размери {w} × {h} пиксела е твърде голямо за рисуване
|
||||
@msg the image can't be read as a texture изображението не може да бъде прочетено като текстура
|
||||
Fill and Stroke Controls Управление на запълването и щриха
|
||||
Coloring Controls Управление на оцветяването
|
||||
Draw Mode Controls Управление на режима на рисуване
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2744,6 +2744,9 @@ effect Soft Měkký
|
||||
Blocks Bloky
|
||||
Frosted Matné sklo
|
||||
Tiny Lens Drobné čočky
|
||||
Loaded Texture Načtená textura
|
||||
Load Texture… Načíst texturu…
|
||||
Texture Image Obraz textury
|
||||
Right Diagonal Pravá diagonála
|
||||
Left Diagonal Levá diagonála
|
||||
effect Horizontal Vodorovný
|
||||
@@ -4839,6 +4842,7 @@ mark-style Japanese Japonské
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} neznámý parametr `{key}`; příkaz přijímá: {keys}
|
||||
Load EPS into Clipboard Načíst EPS do schránky
|
||||
@msg an image of {w} × {h} pixels is too large to draw obraz o velikosti {w} × {h} obrazových bodů je příliš velký na vykreslení
|
||||
@msg the image can't be read as a texture obraz nelze načíst jako texturu
|
||||
Fill and Stroke Controls Ovládací prvky výplně a tahu
|
||||
Coloring Controls Ovládací prvky barev
|
||||
Draw Mode Controls Ovládací prvky režimu kreslení
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4182,6 +4182,9 @@ theme Appearance Erscheinungsbild
|
||||
Blocks Blöcke
|
||||
Frosted Mattiert
|
||||
Tiny Lens Kleine Linsen
|
||||
Loaded Texture Geladene Struktur
|
||||
Load Texture… Struktur laden…
|
||||
Texture Image Strukturbild
|
||||
Canvas Leinwand
|
||||
Highlight Variable Variable hervorheben
|
||||
New Variable Neue Variable
|
||||
@@ -4366,6 +4369,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} unbekannter Parameter `{key}`; der Befehl akzeptiert: {keys}
|
||||
Load EPS into Clipboard EPS in die Zwischenablage laden
|
||||
@msg an image of {w} × {h} pixels is too large to draw ein Bild mit {w} × {h} Pixeln ist zu groß, um gezeichnet zu werden
|
||||
@msg the image can't be read as a texture das Bild kann nicht als Struktur gelesen werden
|
||||
Basic Basis
|
||||
Fill and Stroke Controls Steuerelemente für Fläche und Kontur
|
||||
Coloring Controls Steuerelemente für Farbe
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2034,6 +2034,9 @@ effect Soft Απαλό
|
||||
Blocks Τμήματα
|
||||
Frosted Παγωμένο
|
||||
Tiny Lens Μικροσκοπικός φακός
|
||||
Loaded Texture Φορτωμένη υφή
|
||||
Load Texture… Φόρτωση υφής…
|
||||
Texture Image Εικόνα υφής
|
||||
Text Κείμενο
|
||||
Visibility Ορατότητα
|
||||
Activate Panel Ενεργοποίηση πάνελ
|
||||
@@ -4880,3 +4883,4 @@ effect Points Σημεία
|
||||
@msg the screen color sampler is already open η δειγματοληψία χρώματος από την οθόνη είναι ήδη ανοιχτή
|
||||
@msg this is a PostScript file (an .ai saved in an older format or without PDF compatibility, or an EPS file), not a PDF: File › Open and File › Place read it as EPS αυτό είναι αρχείο PostScript (ένα .ai αποθηκευμένο σε παλαιότερη μορφή ή χωρίς συμβατότητα PDF, ή αρχείο EPS), όχι PDF: οι εντολές Αρχείο › Άνοιγμα και Αρχείο › Τοποθέτηση το διαβάζουν ως EPS
|
||||
@msg this system has no screen color sampler αυτό το σύστημα δεν έχει δειγματοληψία χρώματος από την οθόνη
|
||||
@msg the image can't be read as a texture η εικόνα δεν μπορεί να διαβαστεί ως υφή
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1857 and column 19.
|
@@ -4252,6 +4252,9 @@ axis Both Ambos
|
||||
Blocks Bloques
|
||||
Frosted Esmerilado
|
||||
Tiny Lens Lente pequeña
|
||||
Loaded Texture Textura cargada
|
||||
Load Texture… Cargar textura…
|
||||
Texture Image Imagen de textura
|
||||
Rename Data Set Renombrar conjunto de datos
|
||||
Rename Variable Renombrar variable
|
||||
Variable Options Opciones de variable
|
||||
@@ -4780,6 +4783,7 @@ mark-style Japanese Japonesas
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parámetro desconocido `{key}`; el comando acepta: {keys}
|
||||
Load EPS into Clipboard Cargar EPS en el portapapeles
|
||||
@msg an image of {w} × {h} pixels is too large to draw una imagen de {w} × {h} píxeles es demasiado grande para dibujarse
|
||||
@msg the image can't be read as a texture la imagen no se puede leer como textura
|
||||
Fill and Stroke Controls Controles de relleno y trazo
|
||||
Coloring Controls Controles de color
|
||||
Draw Mode Controls Controles de modo de dibujo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2698,6 +2698,9 @@ effect Soft Pehmeä
|
||||
Blocks Lohkot
|
||||
Frosted Huurrettu
|
||||
Tiny Lens Pieni linssi
|
||||
Loaded Texture Ladattu tekstuuri
|
||||
Load Texture… Lataa tekstuuri…
|
||||
Texture Image Tekstuurikuva
|
||||
Right Diagonal Oikea vinottainen
|
||||
Left Diagonal Vasen vinottainen
|
||||
effect Horizontal Vaaka
|
||||
@@ -4892,3 +4895,4 @@ mark-style Japanese Japanilainen
|
||||
@msg the screen color sampler is already open värin poiminta näytöltä on jo auki
|
||||
@msg this is a PostScript file (an .ai saved in an older format or without PDF compatibility, or an EPS file), not a PDF: File › Open and File › Place read it as EPS tämä on PostScript-tiedosto (vanhemmassa muodossa tai ilman PDF-yhteensopivuutta tallennettu .ai tai EPS-tiedosto), ei PDF: Tiedosto › Avaa ja Tiedosto › Sijoita lukevat sen EPS-muodossa
|
||||
@msg this system has no screen color sampler tässä järjestelmässä ei ole värin poimintaa näytöltä
|
||||
@msg the image can't be read as a texture kuvaa ei voi lukea tekstuurina
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4264,6 +4264,9 @@ effect Soft Doux
|
||||
Blocks Blocs
|
||||
Frosted Dépoli
|
||||
Tiny Lens Petites lentilles
|
||||
Loaded Texture Texture chargée
|
||||
Load Texture… Charger une texture…
|
||||
Texture Image Image de texture
|
||||
Rename Data Set Renommer le jeu de données
|
||||
Rename Variable Renommer la variable
|
||||
Variable Options Options de variable
|
||||
@@ -4839,6 +4842,7 @@ mark-style Japanese Japonais
|
||||
theme Appearance Apparence
|
||||
Load EPS into Clipboard Charger un EPS dans le presse-papiers
|
||||
@msg an image of {w} × {h} pixels is too large to draw une image de {w} × {h} pixels est trop grande pour être dessinée
|
||||
@msg the image can't be read as a texture l'image ne peut pas être lue comme texture
|
||||
Fill and Stroke Controls Commandes de fond et de contour
|
||||
Coloring Controls Commandes de couleur
|
||||
Draw Mode Controls Commandes de mode de dessin
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1991,6 +1991,8 @@
|
||||
Load Profile… Profil betöltése…
|
||||
Load SVG into Clipboard SVG betöltése a vágólapra
|
||||
Load Text into Clipboard Szöveg betöltése a vágólapra
|
||||
Load Texture… Textúra betöltése…
|
||||
Loaded Texture Betöltött textúra
|
||||
Local Libraries Helyi könyvtárak
|
||||
Locate Object Objektum megkeresése
|
||||
Location Hely
|
||||
@@ -3650,6 +3652,7 @@
|
||||
Text Wrap Options Körbefuttatás beállításai
|
||||
Text Wrap Options… Körbefuttatás beállításai…
|
||||
Texture Textúra
|
||||
Texture Image Textúrakép
|
||||
Texturizer Textúrázó
|
||||
Texturizer… Textúrázó…
|
||||
Thai Thai
|
||||
@@ -4806,6 +4809,7 @@ imageTrace Outline Körvonal
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} ismeretlen paraméter: `{key}`; a parancs ezeket fogadja el: {keys}
|
||||
Load EPS into Clipboard EPS betöltése a vágólapra
|
||||
@msg an image of {w} × {h} pixels is too large to draw egy {w} × {h} pixeles kép túl nagy a kirajzoláshoz
|
||||
@msg the image can't be read as a texture a kép nem olvasható be textúraként
|
||||
Fill and Stroke Controls Kitöltés és körvonal vezérlői
|
||||
Coloring Controls Színezés vezérlői
|
||||
Draw Mode Controls Rajzolási mód vezérlői
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4257,6 +4257,9 @@ effect Regular Normale
|
||||
Blocks Blocchi
|
||||
Frosted Smerigliato
|
||||
Tiny Lens Lente piccola
|
||||
Loaded Texture Texture caricata
|
||||
Load Texture… Carica texture…
|
||||
Texture Image Immagine texture
|
||||
Rename Data Set Rinomina set di dati
|
||||
Rename Variable Rinomina variabile
|
||||
Variable Options Opzioni variabile
|
||||
@@ -4411,6 +4414,7 @@ lighting Light Luce
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parametro sconosciuto `{key}`; il comando accetta: {keys}
|
||||
Load EPS into Clipboard Carica EPS negli appunti
|
||||
@msg an image of {w} × {h} pixels is too large to draw un'immagine di {w} × {h} pixel è troppo grande per essere disegnata
|
||||
@msg the image can't be read as a texture l'immagine non può essere letta come texture
|
||||
Basic Base
|
||||
Fill and Stroke Controls Controlli di riempimento e traccia
|
||||
Coloring Controls Controlli colore
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4251,6 +4251,9 @@ effect Vertical 垂直
|
||||
Blocks ブロック
|
||||
Frosted すりガラス
|
||||
Tiny Lens 小さなレンズ
|
||||
Loaded Texture 読み込んだテクスチャ
|
||||
Load Texture… テクスチャを読み込み…
|
||||
Texture Image テクスチャ画像
|
||||
Rename Data Set データセットの名前を変更
|
||||
Variable Options 変数のオプション
|
||||
Edit Data Set データセットを編集
|
||||
@@ -4405,6 +4408,7 @@ lighting Light ライト
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} `{key}` は不明なパラメーターです。このコマンドで使えるパラメーター: {keys}
|
||||
Load EPS into Clipboard EPS をクリップボードに読み込む
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} ピクセルの画像は大きすぎて描画できません
|
||||
@msg the image can't be read as a texture この画像はテクスチャとして読み込めません
|
||||
Basic 基本
|
||||
Fill and Stroke Controls 塗りと線のコントロール
|
||||
Coloring Controls カラーのコントロール
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4229,6 +4229,9 @@ effect Vertical 세로
|
||||
Blocks 블록
|
||||
Frosted 서리 낀 유리
|
||||
Tiny Lens 작은 렌즈
|
||||
Loaded Texture 불러온 텍스처
|
||||
Load Texture… 텍스처 불러오기…
|
||||
Texture Image 텍스처 이미지
|
||||
Rename Data Set 데이터 세트 이름 바꾸기
|
||||
Variable Options 변수 옵션
|
||||
Edit Data Set 데이터 세트 편집
|
||||
@@ -4375,6 +4378,7 @@ lighting Light 광원
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 알 수 없는 매개변수 `{key}`: 이 명령이 받는 매개변수: {keys}
|
||||
Load EPS into Clipboard EPS를 클립보드로 불러오기
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h}픽셀 이미지는 너무 커서 그릴 수 없습니다
|
||||
@msg the image can't be read as a texture 이미지를 텍스처로 읽을 수 없습니다
|
||||
Basic 기본
|
||||
Fill and Stroke Controls 칠 및 획 컨트롤
|
||||
Coloring Controls 색상 컨트롤
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2615,6 +2615,9 @@ effect Soft Miękkie
|
||||
Blocks Bloki
|
||||
Frosted Matowe szkło
|
||||
Tiny Lens Mała soczewka
|
||||
Loaded Texture Wczytana tekstura
|
||||
Load Texture… Wczytaj teksturę…
|
||||
Texture Image Obraz tekstury
|
||||
Right Diagonal Przekątna w prawo
|
||||
Left Diagonal Przekątna w lewo
|
||||
effect Horizontal Poziomo
|
||||
@@ -4022,6 +4025,7 @@ imageTrace Outline Kontur
|
||||
@msg select an Image Trace object zaznacz obiekt obrysu obrazu
|
||||
@msg could not decode image: {0} nie udało się zdekodować obrazu: {0}
|
||||
@msg image is empty obraz jest pusty
|
||||
@msg the image can't be read as a texture nie można odczytać obrazu jako tekstury
|
||||
@msg the image is {width} × {height} pixels, more than Image Trace takes ({max} megapixels) obraz ma {width} × {height} pikseli, więcej niż przyjmuje obrys obrazu ({max} megapikseli)
|
||||
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset ten obrys utworzyłby ponad {max} punktów kontrolnych, więcej niż dokument może płynnie wyświetlić: zwiększ Szum, zmniejsz Ścieżki lub Kolory albo wybierz ustawienie o niższej wierności
|
||||
@msg select an envelope zaznacz kopertę
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4328,6 +4328,9 @@ axis Both Ambos
|
||||
Blocks Blocos
|
||||
Frosted Fosco
|
||||
Tiny Lens Lente Minúscula
|
||||
Loaded Texture Textura carregada
|
||||
Load Texture… Carregar textura…
|
||||
Texture Image Imagem da textura
|
||||
Variable Options Opções de Variável
|
||||
Edit Data Set Editar Conjunto de Dados
|
||||
Accented Edges Bordas Acentuadas
|
||||
@@ -4845,3 +4848,4 @@ mark-style Japanese Japonesas
|
||||
out from a highlighted edge to place its dimension | para fora a partir de uma borda destacada para posicionar a cota |
|
||||
Color Panel Options Opções do Painel Cor
|
||||
@msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` deve ser grayscale, rgb, hsb, cmyk ou websafe
|
||||
@msg the image can't be read as a texture a imagem não pode ser lida como textura
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4253,6 +4253,9 @@ axis Both Оба
|
||||
Blocks Блоки
|
||||
Frosted Матовое
|
||||
Tiny Lens Мелкие линзы
|
||||
Loaded Texture Загруженная текстура
|
||||
Load Texture… Загрузить текстуру…
|
||||
Texture Image Изображение текстуры
|
||||
Rename Data Set Переименовать набор данных
|
||||
Rename Variable Переименовать переменную
|
||||
Variable Options Параметры переменной
|
||||
@@ -4407,6 +4410,7 @@ lighting Light Свет
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} неизвестный параметр `{key}`; команда принимает: {keys}
|
||||
Load EPS into Clipboard Загрузить EPS в буфер обмена
|
||||
@msg an image of {w} × {h} pixels is too large to draw изображение размером {w} × {h} пикселей слишком велико для отрисовки
|
||||
@msg the image can't be read as a texture изображение не удаётся прочитать как текстуру
|
||||
Basic Базовый
|
||||
Fill and Stroke Controls Элементы управления заливкой и обводкой
|
||||
Coloring Controls Элементы управления цветом
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4191,6 +4191,9 @@ effect Soft Mjuk
|
||||
Blocks Block
|
||||
Frosted Frostat
|
||||
Tiny Lens Liten lins
|
||||
Loaded Texture Inläst textur
|
||||
Load Texture… Läs in textur…
|
||||
Texture Image Texturbild
|
||||
Rename Data Set Byt namn på datauppsättning
|
||||
Rename Variable Byt namn på variabel
|
||||
Variable Options Variabelalternativ
|
||||
@@ -4885,3 +4888,4 @@ theme Appearance Utseende
|
||||
@msg the screen color sampler is already open färghämtningen från skärmen är redan öppen
|
||||
@msg this is a PostScript file (an .ai saved in an older format or without PDF compatibility, or an EPS file), not a PDF: File › Open and File › Place read it as EPS det här är en PostScript-fil (en .ai sparad i ett äldre format eller utan PDF-kompatibilitet, eller en EPS-fil), inte en PDF: Fil › Öppna och Fil › Placera läser den som EPS
|
||||
@msg this system has no screen color sampler det här systemet saknar färghämtning från skärmen
|
||||
@msg the image can't be read as a texture bilden kan inte läsas som en textur
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4239,6 +4239,9 @@ axis Both Обидві
|
||||
Blocks Блоки
|
||||
Frosted Матове
|
||||
Tiny Lens Дрібні лінзи
|
||||
Loaded Texture Завантажена текстура
|
||||
Load Texture… Завантажити текстуру…
|
||||
Texture Image Зображення текстури
|
||||
Rename Data Set Перейменувати набір даних
|
||||
Rename Variable Перейменувати змінну
|
||||
Variable Options Параметри змінної
|
||||
@@ -4391,6 +4394,7 @@ lighting Light Світло
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} невідомий параметр `{key}`; команда приймає: {keys}
|
||||
Load EPS into Clipboard Завантажити EPS у буфер обміну
|
||||
@msg an image of {w} × {h} pixels is too large to draw зображення розміром {w} × {h} пікселів завелике для відображення
|
||||
@msg the image can't be read as a texture зображення не вдається прочитати як текстуру
|
||||
Basic Базовий
|
||||
Fill and Stroke Controls Елементи керування заливкою та обведенням
|
||||
Coloring Controls Елементи керування кольором
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 718 and column 40.
|
@@ -1801,6 +1801,8 @@
|
||||
Load Profile… Tải hồ sơ…
|
||||
Load SVG into Clipboard Tải SVG vào bảng tạm
|
||||
Load Text into Clipboard Tải văn bản vào bảng tạm
|
||||
Load Texture… Tải kết cấu…
|
||||
Loaded Texture Kết cấu đã tải
|
||||
Local Libraries Thư viện cục bộ
|
||||
Locate Object Xác định vị trí đối tượng
|
||||
Location Vị trí
|
||||
@@ -3337,6 +3339,7 @@
|
||||
Text Wrap Options Tùy chọn bao văn bản
|
||||
Text Wrap Options… Tùy chọn bao văn bản…
|
||||
Texture Kết cấu
|
||||
Texture Image Ảnh kết cấu
|
||||
Texturizer Texturizer
|
||||
Texturizer… Texturizer…
|
||||
Thai Tiếng Thái
|
||||
@@ -4260,6 +4263,7 @@ theme Appearance Giao diện
|
||||
@msg The graphics were lost and couldn't be restarted. Đồ họa đã mất và không thể khởi động lại.
|
||||
@msg The graphics were lost and have been restarted Đồ họa đã mất và đã được khởi động lại
|
||||
@msg the guide is on a hidden or locked layer đường căn nằm trên lớp ẩn hoặc bị khóa
|
||||
@msg the image can't be read as a texture không thể đọc ảnh làm kết cấu
|
||||
@msg the image is {width} × {height} pixels, more than Image Trace takes ({max} megapixels) ảnh có kích thước {width} × {height} pixel, vượt quá giới hạn vector hóa ảnh ({max} megapixel)
|
||||
@msg the image's pixels don't match its size pixel ảnh không khớp với kích thước
|
||||
@msg the layer has no objects to clip by lớp không có đối tượng để cắt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3729 and column 10.
|
@@ -3851,6 +3851,9 @@ effect Vertical 垂直
|
||||
Blocks 块状
|
||||
Frosted 磨砂
|
||||
Tiny Lens 小镜头
|
||||
Loaded Texture 已载入的纹理
|
||||
Load Texture… 载入纹理…
|
||||
Texture Image 纹理图像
|
||||
Rename Data Set 重命名数据集
|
||||
Rename Variable 重命名变量
|
||||
Variable Options 变量选项
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3635,6 +3635,9 @@ effect Soft 柔和
|
||||
Blocks 塊狀
|
||||
Frosted 磨砂
|
||||
Tiny Lens 小鏡頭
|
||||
Loaded Texture 已載入的紋理
|
||||
Load Texture… 載入紋理…
|
||||
Texture Image 紋理影像
|
||||
Rename Data Set 重新命名資料集
|
||||
Rename Variable 重新命名變數
|
||||
Variable Options 變數選項
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -776,6 +776,11 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, k:
|
||||
change = Some((key.clone(), json!(nx)));
|
||||
}
|
||||
}
|
||||
// Glass's loaded texture shows its size; its dialog's Load Texture… replaces it.
|
||||
Value::String(s) if key == vectorcraft_effects::pixel::TEXTURE_IMAGE => {
|
||||
let size = vectorcraft_effects::pixel::texture(s).map(|t| t.size());
|
||||
widgets::dim_label(ui, &size.map_or_else(String::new, |(w, h)| format!("{w} × {h} px")));
|
||||
}
|
||||
Value::String(s) => {
|
||||
let mut buf = s.clone();
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut buf).desired_width(90.0));
|
||||
|
||||
@@ -72,6 +72,25 @@ fn an_effect_name_opens_its_dialog_prefilled_and_ok_edits_it_in_place() {
|
||||
assert_eq!(effects(&app, None)[0].params["size"], json!(9));
|
||||
}
|
||||
|
||||
/// The inline editor shows Glass's loaded texture by its size, not as its stored text.
|
||||
#[test]
|
||||
fn the_inline_editor_shows_a_loaded_texture_by_its_size() {
|
||||
let mut app = app_with_rect();
|
||||
let mut png = Vec::new();
|
||||
image::GrayImage::from_fn(5, 3, |x, _| image::Luma([(x * 60) as u8]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let file = json!({"name": "t.png", "dataBase64": vectorcraft_format::base64_encode(&png)});
|
||||
run(&mut app, "effect.apply", json!({"effect": "distort.glass", "params": {"textureImage": file}}));
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
set_pstate(&ctx, "ap-fx-open-None-0", true);
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
let stored = effects(&app, None)[0].params["textureImage"].as_str().unwrap().to_string();
|
||||
assert!(texts.iter().any(|(t, _)| t == "5 × 3 px"), "{texts:?}");
|
||||
assert!(!texts.iter().any(|(t, _)| t.contains(&stored[..24])), "the stored text isn't shown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_distorts_inline_editor_opens_its_dialog_for_the_corners() {
|
||||
let mut app = app_with_rect();
|
||||
|
||||
@@ -41,7 +41,7 @@ its resolution, PDF export warns once per object with the requested and the lowe
|
||||
| Artistic | Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | 1/15 (approximate Film Grain) | Colored Pencil, Cutout, Dry Brush, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor |
|
||||
| Blur | Gaussian Blur, Radial Blur, Smart Blur | 3/3 | — |
|
||||
| Brush Strokes | Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e | 8/8 | — |
|
||||
| Distort | Diffuse Glow, Glass, Ocean Ripple | 3/3 | Glass's Load Texture |
|
||||
| Distort | Diffuse Glow, Glass, Ocean Ripple | 3/3 | — |
|
||||
| Pixelate | Color Halftone, Crystallize, Mezzotint, Pointillize | 4/4 | — |
|
||||
| Sharpen | Unsharp Mask | 1/1 | — |
|
||||
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 0/14 | all |
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
## G2 Photoshop-style raster effects and the Effect Gallery
|
||||
|
||||
- **Missing:** 28 of 57 filters (the remaining 14 Artistic filters and all of Sketch), the Effect Gallery dialog, Load
|
||||
Texture for Glass and Texturizer. Measured in
|
||||
Texture for Texturizer. Measured in
|
||||
[effects-parity.md](effects-parity.md).
|
||||
- **Done:** the pixel-filter pipeline and 29 filters (Artistic: Film Grain, Blur, Brush Strokes, Distort, Pixelate, Sharpen, Stylize,
|
||||
Texture, Video), Brush Strokes on 2026-10-11, Film Grain on 2026-10-12, and Smart Blur's Edge Only and Overlay Edge
|
||||
|
||||
+16
@@ -2275,6 +2275,22 @@ fills or strokes carry effects is written as one piece per fill and stroke, so o
|
||||
{"name":"run_command","arguments":{"command":"document.rasterEffectsSettings","params":{"resolution":"high","background":"white","addAround":36}}}
|
||||
```
|
||||
|
||||
## Glass textures
|
||||
|
||||
Glass (`distort.glass`) takes an image file as its surface in `params.textureImage`, as `{path}` or
|
||||
`{name?, dataBase64}`: a PNG, JPEG, GIF, WebP, TIFF, BMP, PSD or PSB file, decoded within the pixel allowance the
|
||||
renderer draws images in, counted as RGBA. `effect.apply` and `effect.setParams` store the image's gray levels in the effect as a grayscale PNG
|
||||
in base64 and set `texture: "loaded"` unless the params give a `texture`. `effect.list` reports the stored string,
|
||||
and a command given that string stores it unchanged. Any other string, a file that is not an image, or an image past
|
||||
the allowance is an error. A save keeps the texture with the effect. Glass lays the texture one image pixel per point
|
||||
at Scaling 100%, centered on the object's center and repeated in both directions. Each pixel's luma is the surface's
|
||||
height, from 0 for black to 1 pt for white. A transparent pixel counts as middle gray, and a partly transparent pixel
|
||||
is mixed with middle gray by its alpha.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"distort.glass","params":{"distortion":10,"textureImage":{"path":"/tmp/texture.png"}}}}}
|
||||
```
|
||||
|
||||
## Linked images
|
||||
|
||||
A placed image linked to its file (`file.place {path}`, Link on) records the file's absolute path, its size,
|
||||
|
||||
@@ -320,7 +320,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Strokes, brushes, width profiles | 5 | 90% | the Art brush's Overlap, auto-generated Pattern brush corners and new tile art in Pattern Brush Options (Brush Options for every brush type done), brush libraries (generated in code), pen pressure on macOS (the windowing layer reports none there), brushes driven by pen tilt, bearing and barrel rotation (6D art pens) | 8–15 |
|
||||
| Appearance, transparency, graphic styles, masks | 5 | 98% | raster effect reach in preview bounds (flattener presets apply in PDF, EPS and print) | 1–2 |
|
||||
| Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap, SVG Filters, the Transform effect's Transform Objects/Patterns and Scale Strokes & Effects options | 6–10 |
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 35% | ~36 Photoshop-style filters (Artistic, Brush Strokes, Sketch, Stylize), Glass's and Texturizer's Load Texture and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur (Normal, Edge Only and Overlay Edge), Unsharp Mask, Glowing Edges, Distort (Diffuse Glow, Glass, Ocean Ripple), Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize), Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer) and Video (De-Interlace, NTSC Colors) are done | 19–31 |
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 35% | ~36 Photoshop-style filters (Artistic, Brush Strokes, Sketch, Stylize), Texturizer's Load Texture and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur (Normal, Edge Only and Overlay Edge), Unsharp Mask, Glowing Edges, Distort (Diffuse Glow, Glass, Ocean Ripple), Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize), Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer) and Video (De-Interlace, NTSC Colors) are done | 19–31 |
|
||||
| 3D and Materials | 4 | ~25% | Initial Revolve, Extrude & Bevel, Inflate and Rotate, with simple lighting; materials, several lights, shadows, ray tracing, 3D Classic and more complete visibility remain | 25–45 |
|
||||
| Type core | 9 | 80% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); Hyphenation… and Justification… options (Every-line Composer for justified and ragged text, per object, is done; it is tuned by eye, not yet compared side by side with Illustrator; mixed type sizes in a paragraph still break single-line), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034) | 14–19 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), custom kinsoku sets (beyond Hard, Soft and None) and the Mojikumi Settings dialog —, manual tate-chu-yoko), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support (beyond parity, not scored: inline symbols in text) | 20–28 |
|
||||
@@ -636,6 +636,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **Moving objects with raster effects (#1257):** during a drag or a dialog's preview, an object whose content only moved draws its cached drop shadows, outer glows and filtered content (Gaussian Blur, Feather and the Photoshop-style filters) from the rasters of an earlier frame at the nearest whole pixel, without running the effects again. The renderer first draws the moved content where the old content lies in its raster and compares the pixels. De-Interlace runs again. The frame after the drag runs the effects again and draws what a fresh renderer draws; on the desktop, the canvas renders that frame on the render worker. `texture_drag_perf` times a drag's frames and the frame after it.
|
||||
|
||||
- **Glass's Load Texture:** Effect › Distort › Glass… has a Load Texture… button that reads a PNG, JPEG, GIF, WebP, TIFF, BMP, PSD or PSB file and adds Loaded Texture to the Texture menu (`textureImage`: `{path}` or `{name?, dataBase64}`, stored in the effect and saved with the document as a grayscale PNG in base64). The texture lies one image pixel per point at Scaling 100%, centered on the object and repeated; each pixel's luma is the surface's height, from 0 for black to 1 pt for white, and a transparent pixel counts as middle gray. Scaling and Invert act on it as on the built-in textures. A Glass saved without `textureImage` draws and saves as before.
|
||||
|
||||
- **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace), Page Knockout Group, Document Setup's substitution highlights, a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes. Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536).
|
||||
|
||||
- **Drop Shadow's Darkness:** Effect › Stylize › Drop Shadow… has Color and Darkness as radio buttons, each with its field, and the Appearance panel's inline editor has the two radio buttons in its Shade row (`shade`: `color` or `darkness`; `darkness`: 0 to 100%, 100 by default). Color is the default, and a shadow saved without these keys draws as before. With Darkness the shadow is the object's own colors with black added: in a CMYK document their cyan, magenta and yellow stay and their black K becomes K + darkness × (1 − K), and in an RGB document each channel is multiplied by 1 − darkness. Darkness 0% gives the object's color; 100% gives black in an RGB document and keeps the colors under 100% black in a CMYK one. SVG export writes the object blurred, moved and darkened with `feComponentTransfer` (screen colors, in a CMYK document too), and SVG import reads it back; CSS export leaves the shadow out and marks the object unsupported. Behavior reference: Illustrator CC Help (2014, [PDF](https://help.adobe.com/archive/en/illustrator/cc/2014/illustrator_reference.pdf)), page 428 (PDF page 434).
|
||||
|
||||
Reference in New Issue
Block a user