@pixagram/renderart 0.4.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +171 -67
  3. package/dist/crt-gpu.d.ts +30 -0
  4. package/dist/crt-gpu.d.ts.map +1 -0
  5. package/dist/crt-gpu.js +282 -0
  6. package/dist/crt-gpu.js.map +1 -0
  7. package/dist/hex-gpu.d.ts +35 -0
  8. package/dist/hex-gpu.d.ts.map +1 -0
  9. package/dist/hex-gpu.js +382 -0
  10. package/dist/hex-gpu.js.map +1 -0
  11. package/dist/index.d.ts +21 -300
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +36 -963
  14. package/dist/index.js.map +1 -1
  15. package/dist/types.d.ts +84 -0
  16. package/dist/types.d.ts.map +1 -0
  17. package/dist/types.js +7 -0
  18. package/dist/types.js.map +1 -0
  19. package/dist/wasm-wrapper.d.ts +71 -0
  20. package/dist/wasm-wrapper.d.ts.map +1 -0
  21. package/dist/wasm-wrapper.js +76 -0
  22. package/dist/wasm-wrapper.js.map +1 -0
  23. package/dist/xbrz-gpu.d.ts +34 -0
  24. package/dist/xbrz-gpu.d.ts.map +1 -0
  25. package/dist/xbrz-gpu.js +640 -0
  26. package/dist/xbrz-gpu.js.map +1 -0
  27. package/package.json +48 -35
  28. package/src/crt-gpu.ts +313 -0
  29. package/src/hex-gpu.ts +426 -0
  30. package/src/index.ts +52 -0
  31. package/src/types.ts +90 -0
  32. package/src/wasm/crt.rs +181 -0
  33. package/src/wasm/hex.rs +324 -0
  34. package/src/wasm/lib.rs +285 -0
  35. package/src/wasm/xbrz.rs +262 -0
  36. package/src/wasm-wrapper.ts +195 -0
  37. package/src/xbrz-gpu.ts +671 -0
  38. package/dist/index.d.mts +0 -305
  39. package/dist/index.mjs +0 -948
  40. package/dist/index.mjs.map +0 -1
  41. package/pkg/LICENSE +0 -21
  42. package/pkg/README.md +0 -117
  43. package/pkg/renderart_wasm.d.ts +0 -52
  44. package/pkg/renderart_wasm.js +0 -5
  45. package/pkg/renderart_wasm_bg.js +0 -283
  46. package/pkg/renderart_wasm_bg.wasm +0 -0
  47. package/pkg/renderart_wasm_bg.wasm.d.ts +0 -24
@@ -0,0 +1,285 @@
1
+ //! RenderArt WASM Module
2
+ //!
3
+ //! High-performance pixel art rendering engines for WebAssembly.
4
+
5
+ use wasm_bindgen::prelude::*;
6
+
7
+ mod crt;
8
+ mod hex;
9
+ mod xbrz;
10
+
11
+ // Static output buffers to avoid deallocation issues
12
+ static mut CRT_BUFFER: Vec<u8> = Vec::new();
13
+ static mut HEX_BUFFER: Vec<u8> = Vec::new();
14
+ static mut XBRZ_BUFFER: Vec<u8> = Vec::new();
15
+
16
+ /// Result of an upscale operation
17
+ #[wasm_bindgen]
18
+ pub struct UpscaleResult {
19
+ pub ptr: u32,
20
+ pub len: u32,
21
+ pub width: u32,
22
+ pub height: u32,
23
+ }
24
+
25
+ /// Get WASM memory for reading output buffers
26
+ #[wasm_bindgen]
27
+ pub fn get_memory() -> JsValue {
28
+ wasm_bindgen::memory()
29
+ }
30
+
31
+ // ============================================================================
32
+ // CRT Functions
33
+ // ============================================================================
34
+
35
+ /// CRT upscale with default config
36
+ #[wasm_bindgen]
37
+ pub fn crt_upscale(data: &[u8], width: u32, height: u32, scale: u32) -> UpscaleResult {
38
+ crt_upscale_config(
39
+ data, width, height, scale,
40
+ 0.015, 0.02, // warp_x, warp_y
41
+ -4.0, 0.5, 0.3, // scan_hardness, scan_opacity, mask_opacity
42
+ true, true, true // enable_warp, enable_scanlines, enable_mask
43
+ )
44
+ }
45
+
46
+ /// CRT upscale with full config
47
+ #[wasm_bindgen]
48
+ pub fn crt_upscale_config(
49
+ data: &[u8],
50
+ width: u32,
51
+ height: u32,
52
+ scale: u32,
53
+ warp_x: f32,
54
+ warp_y: f32,
55
+ scan_hardness: f32,
56
+ scan_opacity: f32,
57
+ mask_opacity: f32,
58
+ enable_warp: bool,
59
+ enable_scanlines: bool,
60
+ enable_mask: bool,
61
+ ) -> UpscaleResult {
62
+ let config = crt::CrtConfig {
63
+ warp_x,
64
+ warp_y,
65
+ scan_hardness,
66
+ scan_opacity,
67
+ mask_opacity,
68
+ enable_warp,
69
+ enable_scanlines,
70
+ enable_mask,
71
+ };
72
+
73
+ let output = crt::crt_upscale(data, width as usize, height as usize, scale as usize, &config);
74
+ let out_width = width * scale;
75
+ let out_height = height * scale;
76
+
77
+ unsafe {
78
+ CRT_BUFFER = output;
79
+ UpscaleResult {
80
+ ptr: CRT_BUFFER.as_ptr() as u32,
81
+ len: CRT_BUFFER.len() as u32,
82
+ width: out_width,
83
+ height: out_height,
84
+ }
85
+ }
86
+ }
87
+
88
+ // ============================================================================
89
+ // HEX Functions
90
+ // ============================================================================
91
+
92
+ /// HEX upscale with default config
93
+ #[wasm_bindgen]
94
+ pub fn hex_upscale(data: &[u8], width: u32, height: u32, scale: u32) -> UpscaleResult {
95
+ hex_upscale_config(
96
+ data, width, height, scale,
97
+ 0, // orientation (flat-top)
98
+ false, // draw_borders
99
+ 0x282828FF, // border_color
100
+ 1, // border_thickness
101
+ 0x00000000 // background_color
102
+ )
103
+ }
104
+
105
+ /// HEX upscale with full config
106
+ #[wasm_bindgen]
107
+ pub fn hex_upscale_config(
108
+ data: &[u8],
109
+ width: u32,
110
+ height: u32,
111
+ scale: u32,
112
+ orientation: u32,
113
+ draw_borders: bool,
114
+ border_color: u32,
115
+ border_thickness: u32,
116
+ background_color: u32,
117
+ ) -> UpscaleResult {
118
+ let config = hex::HexConfig {
119
+ orientation: if orientation == 0 {
120
+ hex::HexOrientation::FlatTop
121
+ } else {
122
+ hex::HexOrientation::PointyTop
123
+ },
124
+ draw_borders,
125
+ border_color,
126
+ border_thickness: border_thickness as usize,
127
+ background_color,
128
+ };
129
+
130
+ let (out_width, out_height) = hex::get_output_dimensions(
131
+ width as usize,
132
+ height as usize,
133
+ scale as usize,
134
+ &config.orientation
135
+ );
136
+
137
+ let output = hex::hex_upscale(data, width as usize, height as usize, scale as usize, &config);
138
+
139
+ unsafe {
140
+ HEX_BUFFER = output;
141
+ UpscaleResult {
142
+ ptr: HEX_BUFFER.as_ptr() as u32,
143
+ len: HEX_BUFFER.len() as u32,
144
+ width: out_width as u32,
145
+ height: out_height as u32,
146
+ }
147
+ }
148
+ }
149
+
150
+ /// Get HEX output dimensions
151
+ #[wasm_bindgen]
152
+ pub fn hex_get_dimensions(width: u32, height: u32, scale: u32, orientation: u32) -> Vec<u32> {
153
+ let orient = if orientation == 0 {
154
+ hex::HexOrientation::FlatTop
155
+ } else {
156
+ hex::HexOrientation::PointyTop
157
+ };
158
+
159
+ let (out_w, out_h) = hex::get_output_dimensions(
160
+ width as usize,
161
+ height as usize,
162
+ scale as usize,
163
+ &orient
164
+ );
165
+
166
+ vec![out_w as u32, out_h as u32]
167
+ }
168
+
169
+ // ============================================================================
170
+ // XBRZ Functions
171
+ // ============================================================================
172
+
173
+ /// XBRZ upscale with default config
174
+ #[wasm_bindgen]
175
+ pub fn xbrz_upscale(data: &[u8], width: u32, height: u32, scale: u32) -> UpscaleResult {
176
+ xbrz_upscale_config(
177
+ data, width, height, scale,
178
+ 1.0, // luminance_weight
179
+ 30, // equal_color_tolerance
180
+ 4.4, // dominant_direction_threshold
181
+ 2.2 // steep_direction_threshold
182
+ )
183
+ }
184
+
185
+ /// XBRZ upscale with full config
186
+ #[wasm_bindgen]
187
+ pub fn xbrz_upscale_config(
188
+ data: &[u8],
189
+ width: u32,
190
+ height: u32,
191
+ scale: u32,
192
+ luminance_weight: f32,
193
+ equal_color_tolerance: u32,
194
+ dominant_direction_threshold: f32,
195
+ steep_direction_threshold: f32,
196
+ ) -> UpscaleResult {
197
+ let config = xbrz::XbrzConfig {
198
+ luminance_weight,
199
+ equal_color_tolerance,
200
+ dominant_direction_threshold,
201
+ steep_direction_threshold,
202
+ };
203
+
204
+ let clamped_scale = scale.clamp(2, 6) as usize;
205
+ let output = xbrz::xbrz_upscale(data, width as usize, height as usize, clamped_scale, &config);
206
+
207
+ let out_width = width * clamped_scale as u32;
208
+ let out_height = height * clamped_scale as u32;
209
+
210
+ unsafe {
211
+ XBRZ_BUFFER = output;
212
+ UpscaleResult {
213
+ ptr: XBRZ_BUFFER.as_ptr() as u32,
214
+ len: XBRZ_BUFFER.len() as u32,
215
+ width: out_width,
216
+ height: out_height,
217
+ }
218
+ }
219
+ }
220
+
221
+ // ============================================================================
222
+ // Tests
223
+ // ============================================================================
224
+
225
+ #[cfg(test)]
226
+ mod tests {
227
+ use super::*;
228
+
229
+ fn create_test_image(w: usize, h: usize) -> Vec<u8> {
230
+ let mut data = vec![0u8; w * h * 4];
231
+ for y in 0..h {
232
+ for x in 0..w {
233
+ let i = (y * w + x) * 4;
234
+ data[i] = (x * 255 / w) as u8; // R
235
+ data[i + 1] = (y * 255 / h) as u8; // G
236
+ data[i + 2] = 128; // B
237
+ data[i + 3] = 255; // A
238
+ }
239
+ }
240
+ data
241
+ }
242
+
243
+ #[test]
244
+ fn test_crt_basic() {
245
+ let img = create_test_image(4, 4);
246
+ let result = crt_upscale(&img, 4, 4, 2);
247
+ assert_eq!(result.width, 8);
248
+ assert_eq!(result.height, 8);
249
+ assert_eq!(result.len, 8 * 8 * 4);
250
+ }
251
+
252
+ #[test]
253
+ fn test_hex_dimensions() {
254
+ let dims = hex_get_dimensions(4, 4, 16, 0);
255
+ assert!(dims[0] > 0);
256
+ assert!(dims[1] > 0);
257
+ }
258
+
259
+ #[test]
260
+ fn test_hex_render() {
261
+ let img = create_test_image(4, 4);
262
+ let result = hex_upscale(&img, 4, 4, 8);
263
+ assert!(result.width > 0);
264
+ assert!(result.height > 0);
265
+ }
266
+
267
+ #[test]
268
+ fn test_xbrz_basic() {
269
+ let img = create_test_image(4, 4);
270
+ let result = xbrz_upscale(&img, 4, 4, 2);
271
+ assert_eq!(result.width, 8);
272
+ assert_eq!(result.height, 8);
273
+ }
274
+
275
+ #[test]
276
+ fn test_xbrz_scale_factors() {
277
+ let img = create_test_image(4, 4);
278
+
279
+ for scale in 2..=6 {
280
+ let result = xbrz_upscale(&img, 4, 4, scale);
281
+ assert_eq!(result.width, 4 * scale);
282
+ assert_eq!(result.height, 4 * scale);
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,262 @@
1
+ //! xBRZ Pixel Art Scaling Engine
2
+
3
+ /// XBRZ configuration
4
+ #[derive(Clone, Copy)]
5
+ pub struct XbrzConfig {
6
+ pub luminance_weight: f32,
7
+ pub equal_color_tolerance: u32,
8
+ pub dominant_direction_threshold: f32,
9
+ pub steep_direction_threshold: f32,
10
+ }
11
+
12
+ impl Default for XbrzConfig {
13
+ fn default() -> Self {
14
+ Self {
15
+ luminance_weight: 1.0,
16
+ equal_color_tolerance: 30,
17
+ dominant_direction_threshold: 4.4,
18
+ steep_direction_threshold: 2.2,
19
+ }
20
+ }
21
+ }
22
+
23
+ #[derive(Clone, Copy, Default)]
24
+ struct Color {
25
+ r: u8,
26
+ g: u8,
27
+ b: u8,
28
+ a: u8,
29
+ }
30
+
31
+ impl Color {
32
+ #[inline(always)]
33
+ fn from_rgba(data: &[u8], idx: usize) -> Self {
34
+ Self {
35
+ r: data[idx],
36
+ g: data[idx + 1],
37
+ b: data[idx + 2],
38
+ a: data[idx + 3],
39
+ }
40
+ }
41
+
42
+ #[inline(always)]
43
+ fn write_to(&self, data: &mut [u8], idx: usize) {
44
+ data[idx] = self.r;
45
+ data[idx + 1] = self.g;
46
+ data[idx + 2] = self.b;
47
+ data[idx + 3] = self.a;
48
+ }
49
+
50
+ #[inline(always)]
51
+ fn luminance(&self) -> u32 {
52
+ (self.r as u32 * 299 + self.g as u32 * 587 + self.b as u32 * 114) / 1000
53
+ }
54
+
55
+ #[inline(always)]
56
+ fn blend(c1: Color, c2: Color, ratio: u32) -> Color {
57
+ let inv_ratio = 256 - ratio;
58
+ Color {
59
+ r: ((c1.r as u32 * inv_ratio + c2.r as u32 * ratio) >> 8) as u8,
60
+ g: ((c1.g as u32 * inv_ratio + c2.g as u32 * ratio) >> 8) as u8,
61
+ b: ((c1.b as u32 * inv_ratio + c2.b as u32 * ratio) >> 8) as u8,
62
+ a: ((c1.a as u32 * inv_ratio + c2.a as u32 * ratio) >> 8) as u8,
63
+ }
64
+ }
65
+ }
66
+
67
+ #[inline(always)]
68
+ fn color_distance(c1: Color, c2: Color, lum_weight: f32) -> f32 {
69
+ let dr = (c1.r as i32 - c2.r as i32).abs() as f32;
70
+ let dg = (c1.g as i32 - c2.g as i32).abs() as f32;
71
+ let db = (c1.b as i32 - c2.b as i32).abs() as f32;
72
+ let da = (c1.a as i32 - c2.a as i32).abs() as f32;
73
+
74
+ let lum1 = c1.luminance() as f32;
75
+ let lum2 = c2.luminance() as f32;
76
+ let dl = (lum1 - lum2).abs();
77
+
78
+ (dr * 0.299 + dg * 0.587 + db * 0.114) * (1.0 - lum_weight) + dl * lum_weight + da * 0.5
79
+ }
80
+
81
+ #[inline(always)]
82
+ fn colors_equal(c1: Color, c2: Color, tolerance: u32) -> bool {
83
+ let dr = (c1.r as i32 - c2.r as i32).abs() as u32;
84
+ let dg = (c1.g as i32 - c2.g as i32).abs() as u32;
85
+ let db = (c1.b as i32 - c2.b as i32).abs() as u32;
86
+ let da = (c1.a as i32 - c2.a as i32).abs() as u32;
87
+
88
+ dr + dg + db + da <= tolerance * 4
89
+ }
90
+
91
+ /// Core xBRZ rendering function
92
+ pub fn xbrz_upscale(
93
+ input: &[u8],
94
+ src_w: usize,
95
+ src_h: usize,
96
+ scale: usize,
97
+ config: &XbrzConfig,
98
+ ) -> Vec<u8> {
99
+ let scale = scale.clamp(2, 6);
100
+ let out_w = src_w * scale;
101
+ let out_h = src_h * scale;
102
+ let mut output = vec![0u8; out_w * out_h * 4];
103
+
104
+ for sy in 0..src_h {
105
+ for sx in 0..src_w {
106
+ let kernel = get_kernel(input, src_w, src_h, sx as i32, sy as i32);
107
+ let scaled = scale_pixel(&kernel, scale, config);
108
+
109
+ for by in 0..scale {
110
+ for bx in 0..scale {
111
+ let ox = sx * scale + bx;
112
+ let oy = sy * scale + by;
113
+ let out_idx = (oy * out_w + ox) * 4;
114
+ scaled[by * scale + bx].write_to(&mut output, out_idx);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ output
121
+ }
122
+
123
+ fn get_kernel(input: &[u8], w: usize, h: usize, x: i32, y: i32) -> [Color; 9] {
124
+ let mut kernel = [Color::default(); 9];
125
+
126
+ for dy in -1..=1i32 {
127
+ for dx in -1..=1i32 {
128
+ let sx = (x + dx).clamp(0, w as i32 - 1) as usize;
129
+ let sy = (y + dy).clamp(0, h as i32 - 1) as usize;
130
+ let idx = (sy * w + sx) * 4;
131
+ let ki = ((dy + 1) * 3 + (dx + 1)) as usize;
132
+ kernel[ki] = Color::from_rgba(input, idx);
133
+ }
134
+ }
135
+
136
+ kernel
137
+ }
138
+
139
+ fn scale_pixel(kernel: &[Color; 9], scale: usize, config: &XbrzConfig) -> Vec<Color> {
140
+ let center = kernel[4];
141
+ let mut result = vec![center; scale * scale];
142
+
143
+ process_corner(
144
+ &mut result, scale, 0, 0,
145
+ kernel[0], kernel[1], kernel[3], center,
146
+ config,
147
+ );
148
+
149
+ process_corner(
150
+ &mut result, scale, scale as i32 - 1, 0,
151
+ kernel[2], kernel[1], kernel[5], center,
152
+ config,
153
+ );
154
+
155
+ process_corner(
156
+ &mut result, scale, 0, scale as i32 - 1,
157
+ kernel[6], kernel[7], kernel[3], center,
158
+ config,
159
+ );
160
+
161
+ process_corner(
162
+ &mut result, scale, scale as i32 - 1, scale as i32 - 1,
163
+ kernel[8], kernel[7], kernel[5], center,
164
+ config,
165
+ );
166
+
167
+ result
168
+ }
169
+
170
+ #[allow(clippy::too_many_arguments)]
171
+ fn process_corner(
172
+ result: &mut [Color],
173
+ scale: usize,
174
+ cx: i32, cy: i32,
175
+ corner: Color,
176
+ edge1: Color,
177
+ edge2: Color,
178
+ center: Color,
179
+ config: &XbrzConfig,
180
+ ) {
181
+ let lum_w = config.luminance_weight;
182
+ let eq_tol = config.equal_color_tolerance;
183
+
184
+ let e1_eq_c = colors_equal(edge1, center, eq_tol);
185
+ let e2_eq_c = colors_equal(edge2, center, eq_tol);
186
+ let corner_eq_e1 = colors_equal(corner, edge1, eq_tol);
187
+ let corner_eq_e2 = colors_equal(corner, edge2, eq_tol);
188
+
189
+ let dist_e1_e2 = color_distance(edge1, edge2, lum_w);
190
+ let dist_e1_c = color_distance(edge1, center, lum_w);
191
+ let dist_e2_c = color_distance(edge2, center, lum_w);
192
+
193
+ if !e1_eq_c && !e2_eq_c {
194
+ let weight = determine_blend_weight(
195
+ dist_e1_c, dist_e2_c, dist_e1_e2,
196
+ corner_eq_e1, corner_eq_e2,
197
+ config,
198
+ );
199
+
200
+ if weight > 0.0 {
201
+ let blend_color = if dist_e1_c < dist_e2_c { edge1 } else { edge2 };
202
+ let ratio = (weight * 256.0) as u32;
203
+
204
+ let idx = (cy * scale as i32 + cx) as usize;
205
+ result[idx] = Color::blend(center, blend_color, ratio.min(192));
206
+
207
+ if scale >= 3 {
208
+ blend_adjacent(result, scale, cx, cy, blend_color, weight * 0.5);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ fn determine_blend_weight(
215
+ dist1: f32,
216
+ dist2: f32,
217
+ dist_between: f32,
218
+ _eq1: bool,
219
+ _eq2: bool,
220
+ config: &XbrzConfig,
221
+ ) -> f32 {
222
+ let ratio = if dist1 < 0.001 || dist2 < 0.001 {
223
+ 0.0
224
+ } else if dist1 < dist2 {
225
+ dist2 / dist1
226
+ } else {
227
+ dist1 / dist2
228
+ };
229
+
230
+ if ratio > config.dominant_direction_threshold {
231
+ 0.75
232
+ } else if ratio > config.steep_direction_threshold {
233
+ 0.5
234
+ } else if dist_between > (dist1 + dist2) * 0.5 {
235
+ 0.25
236
+ } else {
237
+ 0.0
238
+ }
239
+ }
240
+
241
+ fn blend_adjacent(
242
+ result: &mut [Color],
243
+ scale: usize,
244
+ cx: i32, cy: i32,
245
+ blend: Color,
246
+ weight: f32,
247
+ ) {
248
+ let s = scale as i32;
249
+ let ratio = (weight * 128.0) as u32;
250
+
251
+ let hx = if cx == 0 { 1 } else { cx - 1 };
252
+ if hx >= 0 && hx < s {
253
+ let idx = (cy * s + hx) as usize;
254
+ result[idx] = Color::blend(result[idx], blend, ratio);
255
+ }
256
+
257
+ let vy = if cy == 0 { 1 } else { cy - 1 };
258
+ if vy >= 0 && vy < s {
259
+ let idx = (vy * s + cx) as usize;
260
+ result[idx] = Color::blend(result[idx], blend, ratio);
261
+ }
262
+ }