mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
feat(pet): vendored ratatui pet elements, DotWhale (committed as-is at the founder's instruction; not yet built or tested by the committer)
Pet-model work in progress: DotWhale component, PetMode/WhalePet/avatar sprite/color changes, UPSTREAM.json refresh. Build, nextest and ratatui sync with the sibling repo are still to be run; do not push until they have been. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
4d1717cf05
commit
dc8884455d
9 files changed
+372
-36
No files matched your search
+16
@@ -185,5 +185,21 @@
|
||||
"tests/fixtures/whale-motion/scenario-conformance.json": "b00bbfbe2c65ce7fb86f803599a0e04de875d6dffd8ac39f87932f5af8447198",
|
||||
"tests/fixtures/whale-motion/scenarios.json": "2386a509089f1a1096398252cb526048200cf116647b03f4dc366bc7d202e381",
|
||||
"vendor/codewhale-design/tokens.rs": "975b7a4e2cda30f9dfa642283f092b9770529c5ec8c401d05b9bde5b6e55bdd1"
|
||||
},
|
||||
"pet_update": {
|
||||
"commit": "00c6505356c5264fa6fd7d01f66458624d03e951",
|
||||
"scope": "Selected prepared pet renderer files; non-pet components retain the base snapshot and existing Engine patches.",
|
||||
"files": {
|
||||
"src/avatar_sprite.rs": "efa14ec46bae22772f01fa28375c4f75cb8fe3696ca29f392b157b6173e1adee",
|
||||
"src/color.rs": "96c44be1540c4756f251ed5ffb74aa88a5ebf131b104bf25866ad7ab8e3c6ba8",
|
||||
"src/components/dot_whale.rs": "191bacf808037ba4dd7d4dfffbd06fe9c951a258a31495380591ad6ab4a715fa",
|
||||
"src/components/pet_mode.rs": "35142d709073a09720f1ce1cfb57b18d6ea4880530a5c8d96755c5b9fc203214",
|
||||
"src/components/whale_pet.rs": "bb29442f8db20e8387482e2e1dcb4fa5259ef99c3a37a77bfe9e4b56eaa05aed",
|
||||
"src/whale.rs": "126a6efc8cac550ccd7f9c8edc61745b8c959186de118c2f4d479f5a5c5ffcf7",
|
||||
"src/whale_motion/pixels.rs": "f232f48a1a9c36339b3dd78e9c5ae5a2043d08749d023977bc39633821ad0bb6"
|
||||
},
|
||||
"merged_exports": {
|
||||
"src/components/mod.rs": "0ea81b474b2137b177c5535e7523ed6b312ea972630ac94f5131883246496e8e"
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-7
@@ -22,7 +22,8 @@ pub struct Sprite<'a> {
|
||||
frame: usize,
|
||||
}
|
||||
impl<'a> Sprite<'a> {
|
||||
/// One bounded RGBA image, such as the canonical pet's live cove.
|
||||
/// One already-decoded RGBA image, without an avatar manifest. Useful for
|
||||
/// live vector scenes; shares the same half-block and capability painter.
|
||||
pub fn image(pixels: &'a [u8], width: usize, height: usize) -> Result<Self, String> {
|
||||
Self::tiles(pixels, width, height, 0, 1)
|
||||
}
|
||||
@@ -91,6 +92,45 @@ impl<'a> Sprite<'a> {
|
||||
.try_into()
|
||||
.expect("validated RGBA pixel")
|
||||
}
|
||||
|
||||
fn sample(&self, x: usize, y: usize, width: usize, height: usize) -> [u8; 4] {
|
||||
if width == self.width && height == self.height {
|
||||
return self.pixel(x, y);
|
||||
}
|
||||
let bounds = |position: usize, source: usize, target: usize| {
|
||||
let scale = source as f64 / target as f64;
|
||||
let center = (position as f64 + 0.5) * scale;
|
||||
let radius = scale.max(1.) / 2.;
|
||||
(
|
||||
(center - radius).max(0.),
|
||||
(center + radius).min(source as f64),
|
||||
)
|
||||
};
|
||||
let (left, right) = bounds(x, self.width, width);
|
||||
let (top, bottom) = bounds(y, self.height, height);
|
||||
let mut rgba = [0.; 4];
|
||||
for sy in top.floor() as usize..(bottom.ceil() as usize).min(self.height) {
|
||||
let weight_y = (bottom.min(sy as f64 + 1.) - top.max(sy as f64)).max(0.);
|
||||
for sx in left.floor() as usize..(right.ceil() as usize).min(self.width) {
|
||||
let weight_x = (right.min(sx as f64 + 1.) - left.max(sx as f64)).max(0.);
|
||||
let pixel = self.pixel(sx, sy);
|
||||
let alpha = f64::from(pixel[3]) * weight_x * weight_y;
|
||||
for channel in 0..3 {
|
||||
rgba[channel] += f64::from(pixel[channel]) * alpha;
|
||||
}
|
||||
rgba[3] += alpha;
|
||||
}
|
||||
}
|
||||
if rgba[3] == 0. {
|
||||
return [0; 4];
|
||||
}
|
||||
[
|
||||
(rgba[0] / rgba[3]).round() as u8,
|
||||
(rgba[1] / rgba[3]).round() as u8,
|
||||
(rgba[2] / rgba[3]).round() as u8,
|
||||
(rgba[3] / ((right - left) * (bottom - top))).round() as u8,
|
||||
]
|
||||
}
|
||||
}
|
||||
impl Paint for Sprite<'_> {
|
||||
fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
@@ -113,13 +153,16 @@ impl Paint for Sprite<'_> {
|
||||
let colored = theme.caps().paints_tokens() && !theme.ascii();
|
||||
for y in 0..rows {
|
||||
for x in 0..width {
|
||||
let sx = (usize::from(x) * self.width / usize::from(width)).min(self.width - 1);
|
||||
let sy = |half: usize| {
|
||||
((usize::from(y) * 2 + half) * self.height / (usize::from(rows) * 2))
|
||||
.min(self.height - 1)
|
||||
let sample = |half: usize| {
|
||||
self.sample(
|
||||
usize::from(x),
|
||||
usize::from(y) * 2 + half,
|
||||
usize::from(width),
|
||||
usize::from(rows) * 2,
|
||||
)
|
||||
};
|
||||
let a = self.pixel(sx, sy(0));
|
||||
let b = self.pixel(sx, sy(1));
|
||||
let a = sample(0);
|
||||
let b = sample(1);
|
||||
if a[3] < 40 && b[3] < 40 {
|
||||
continue;
|
||||
}
|
||||
|
||||
+34
-1
@@ -7,7 +7,9 @@
|
||||
//! here name roles, and [`crate::theme::Theme`] resolves a role for the
|
||||
//! terminal's depth when it paints.
|
||||
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::{Color, Style};
|
||||
|
||||
use crate::theme::{Role, Theme};
|
||||
|
||||
/// How many colors the terminal can show.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -205,6 +207,37 @@ pub fn contrast_ratio(fg: Color, bg: Color) -> Option<f32> {
|
||||
Some((hi + 0.05) / (lo + 0.05))
|
||||
}
|
||||
|
||||
pub(crate) fn prepared_ink(
|
||||
pigment: [u8; 3],
|
||||
alpha: f32,
|
||||
background: Color,
|
||||
theme: &Theme,
|
||||
) -> Style {
|
||||
if !theme.paints_grounds() {
|
||||
return theme.fg(Role::Primary);
|
||||
}
|
||||
let ground = resolvable_rgb(background)
|
||||
.map(|(r, g, b)| Color::Rgb(r, g, b))
|
||||
.unwrap_or_else(|| theme.token(Role::Background));
|
||||
let base = blend(
|
||||
Color::Rgb(pigment[0], pigment[1], pigment[2]),
|
||||
ground,
|
||||
alpha,
|
||||
);
|
||||
let quantize = |color| match (theme.depth(), color) {
|
||||
(ColorDepth::Ansi256, Color::Rgb(r, g, b)) => Color::Indexed(rgb_to_ansi256(r, g, b)),
|
||||
(_, color) => color,
|
||||
};
|
||||
let mut result = quantize(base);
|
||||
for step in 1..=8 {
|
||||
if contrast_ratio(result, ground).is_none_or(|ratio| ratio >= 3.0) {
|
||||
break;
|
||||
}
|
||||
result = quantize(blend(theme.token(Role::Primary), base, step as f32 / 8.0));
|
||||
}
|
||||
Style::default().fg(result)
|
||||
}
|
||||
|
||||
/// `hex` moved `amount` of the way toward `toward`, then rescaled so its
|
||||
/// WCAG relative luminance stays `hex`'s. Contrast depends only on
|
||||
/// luminance, so every contrast pair audited for `hex` holds for the result
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style},
|
||||
widgets::{Paragraph, Widget, Wrap},
|
||||
};
|
||||
|
||||
use crate::{Paint, Role, Theme, color, text};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct DotWhale<'a> {
|
||||
points: &'a [[f64; 2]],
|
||||
materials: &'a [[f64; 4]],
|
||||
caption: &'a str,
|
||||
action_id: Option<&'a str>,
|
||||
hollow: bool,
|
||||
}
|
||||
|
||||
impl<'a> DotWhale<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(points: &'a [[f64; 2]], materials: &'a [[f64; 4]]) -> Self {
|
||||
Self {
|
||||
points,
|
||||
materials,
|
||||
caption: "Activity unobserved",
|
||||
action_id: None,
|
||||
hollow: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn caption(mut self, caption: &'a str) -> Self {
|
||||
self.caption = caption;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn action_id(mut self, action_id: Option<&'a str>) -> Self {
|
||||
self.action_id = action_id;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn hollow(mut self, hollow: bool) -> Self {
|
||||
self.hollow = hollow;
|
||||
self
|
||||
}
|
||||
|
||||
fn valid(&self) -> bool {
|
||||
!self.points.is_empty()
|
||||
&& self.points.len() <= 980
|
||||
&& self.points.len() == self.materials.len()
|
||||
&& self
|
||||
.points
|
||||
.iter()
|
||||
.flatten()
|
||||
.all(|n| n.is_finite() && (-1.0..=1.0).contains(n))
|
||||
&& self.materials.iter().all(|m| {
|
||||
m[..3]
|
||||
.iter()
|
||||
.all(|n| n.is_finite() && (0.0..=255.0).contains(n))
|
||||
&& m[3].is_finite()
|
||||
&& (0.0..=1.0).contains(&m[3])
|
||||
})
|
||||
}
|
||||
|
||||
fn label(&self, theme: &Theme) -> String {
|
||||
let caption = text::display_safe(self.caption);
|
||||
let caption = text::truncate(&caption, 192, theme.ascii());
|
||||
let Some(action) = self.action_id.filter(|id| !id.is_empty()) else {
|
||||
return caption.into_owned();
|
||||
};
|
||||
let action = text::display_safe(action);
|
||||
let action = text::truncate(&action, 256, theme.ascii());
|
||||
if caption.is_empty() {
|
||||
action.into_owned()
|
||||
} else {
|
||||
format!(
|
||||
"{caption} {} {action}",
|
||||
if theme.ascii() { "/" } else { "·" }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DotCell {
|
||||
bits: u8,
|
||||
pigment: [f64; 3],
|
||||
weight: f64,
|
||||
count: u16,
|
||||
}
|
||||
|
||||
fn caption(label: &str, area: Rect, buf: &mut Buffer, style: Style) {
|
||||
let width = area.width.min(512);
|
||||
if width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let paragraph = Paragraph::new(label)
|
||||
.style(style)
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: false });
|
||||
let height = paragraph.line_count(width).min(usize::from(area.height)) as u16;
|
||||
if height == 0 {
|
||||
return;
|
||||
}
|
||||
let x = u32::from(area.x) + u32::from((area.width - width) / 2);
|
||||
let y = u32::from(area.y) + u32::from((area.height - height) / 2);
|
||||
let mut local = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
paragraph.render(local.area, &mut local);
|
||||
for row in 0..height {
|
||||
for col in 0..width {
|
||||
let (Ok(x), Ok(y)) = (
|
||||
u16::try_from(x + u32::from(col)),
|
||||
u16::try_from(y + u32::from(row)),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||
let background = cell.bg;
|
||||
*cell = local[(col, row)].clone();
|
||||
cell.set_bg(background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Paint for DotWhale<'_> {
|
||||
fn height(&self, width: u16, theme: &Theme) -> u16 {
|
||||
if width == 0 {
|
||||
return 0;
|
||||
}
|
||||
let label = self.label(theme);
|
||||
let rows = Paragraph::new(label)
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(width.min(512))
|
||||
.min(512) as u16;
|
||||
if theme.ascii() || width < 12 || !self.valid() {
|
||||
rows
|
||||
} else {
|
||||
rows.saturating_add((width / 2).clamp(4, 20))
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
if area.is_empty() || area.intersection(buf.area).is_empty() {
|
||||
return;
|
||||
}
|
||||
let label = self.label(theme);
|
||||
let label_height = Paragraph::new(label.as_str())
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(area.width.min(512))
|
||||
.min(usize::from(area.height)) as u16;
|
||||
let art_height = area.height.saturating_sub(label_height);
|
||||
if theme.ascii() || area.width < 12 || art_height < 4 || !self.valid() {
|
||||
caption(&label, area, buf, theme.fg(Role::Primary));
|
||||
return;
|
||||
}
|
||||
let width = f64::from(area.width) * 2.0;
|
||||
let height = f64::from(art_height) * 4.0;
|
||||
let scale = (width - 1.0).min(height - 1.0) / 2.0;
|
||||
let bits = [[1, 8], [2, 16], [4, 32], [64, 128]];
|
||||
let mut cells: BTreeMap<(u16, u16), DotCell> = BTreeMap::new();
|
||||
for (point, material) in self.points.iter().zip(self.materials) {
|
||||
if material[3] <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let x = ((width - 1.0) / 2.0 + point[0] * scale).round() as u32;
|
||||
let y = ((height - 1.0) / 2.0 + point[1] * scale).round() as u32;
|
||||
let (Ok(col), Ok(row)) = (
|
||||
u16::try_from(u32::from(area.x) + x / 2),
|
||||
u16::try_from(u32::from(area.y) + y / 4),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if !buf.area.contains((col, row).into()) {
|
||||
continue;
|
||||
}
|
||||
let cell = cells.entry((col, row)).or_default();
|
||||
cell.bits |= bits[(y % 4) as usize][(x % 2) as usize];
|
||||
for (sum, value) in cell.pigment.iter_mut().zip(material) {
|
||||
*sum += value * material[3];
|
||||
}
|
||||
cell.weight += material[3];
|
||||
cell.count += 1;
|
||||
}
|
||||
for ((x, y), dot) in cells {
|
||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||
let [r, g, b] = dot.pigment.map(|sum| (sum / dot.weight).round() as u8);
|
||||
let mut style = color::prepared_ink(
|
||||
[r, g, b],
|
||||
(dot.weight / f64::from(dot.count)) as f32,
|
||||
cell.bg,
|
||||
theme,
|
||||
);
|
||||
if self.hollow {
|
||||
style = style.add_modifier(Modifier::DIM);
|
||||
}
|
||||
let glyph = char::from_u32(0x2800 + u32::from(dot.bits)).unwrap_or(' ');
|
||||
let mut encoded = [0; 4];
|
||||
cell.set_symbol(glyph.encode_utf8(&mut encoded))
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
caption(
|
||||
&label,
|
||||
Rect::new(
|
||||
area.x,
|
||||
area.y.saturating_add(art_height),
|
||||
area.width,
|
||||
label_height,
|
||||
),
|
||||
buf,
|
||||
theme.fg(Role::Primary),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -123,5 +123,8 @@ mod whale_pet;
|
||||
pub use whale_pet::*;
|
||||
mod subagents;
|
||||
pub use subagents::*;
|
||||
mod dot_whale;
|
||||
pub use dot_whale::*;
|
||||
|
||||
mod pet_mode;
|
||||
pub use pet_mode::*;
|
||||
+14
-7
@@ -2,8 +2,8 @@
|
||||
//! the host still owns sessions, roster receipts, responses and every action.
|
||||
|
||||
use crate::{
|
||||
MotionMode, Role, StatusMark, Subagent, SubagentIntent, SubagentView, SubagentViewState,
|
||||
SubagentViewWords, Theme, WhalePet, text,
|
||||
DotWhale, MotionMode, Paint, Role, StatusMark, Subagent, SubagentIntent, SubagentView,
|
||||
SubagentViewState, SubagentViewWords, Theme, WhalePet, text,
|
||||
whale_motion::{Inputs, Presence, Stage, Tier},
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind};
|
||||
@@ -68,7 +68,7 @@ impl PetModeAreas {
|
||||
} else {
|
||||
body
|
||||
};
|
||||
if output && main.height >= 8 {
|
||||
if output && main.height >= 27 && main.width >= 16 {
|
||||
// Reading has priority over scenery, including at 80x24.
|
||||
let height = (u32::from(main.height) * 2 / 3).max(6) as u16;
|
||||
plan.output = Rect::new(main.x, main.bottom() - height, main.width, height);
|
||||
@@ -155,7 +155,8 @@ impl PetModeState {
|
||||
if !self.visible || !self.motion.animates() {
|
||||
return None;
|
||||
}
|
||||
let pet = (!self.areas.pet.is_empty() && self.stage_animates)
|
||||
let art = WhalePet::art_area(self.areas.pet);
|
||||
let pet = (!theme.ascii() && art.width >= 16 && art.height >= 8 && self.stage_animates)
|
||||
.then(|| {
|
||||
self.stage.cadence(if theme.paints_grounds() {
|
||||
Tier::Hero
|
||||
@@ -316,6 +317,7 @@ pub struct PetMode<'a> {
|
||||
pub theme: &'a Theme,
|
||||
pub title: Cow<'a, str>,
|
||||
pub status: StatusMark,
|
||||
pub dot_whale: Option<DotWhale<'a>>,
|
||||
/// The host's existing notice, using its semantic severity ink.
|
||||
pub notice: Option<Line<'a>>,
|
||||
pub agents: &'a [Subagent<'a>],
|
||||
@@ -334,6 +336,7 @@ impl<'a> PetMode<'a> {
|
||||
theme,
|
||||
title: "Codewhale".into(),
|
||||
status,
|
||||
dot_whale: None,
|
||||
notice: None,
|
||||
agents: &[],
|
||||
agent_words: SubagentViewWords::default(),
|
||||
@@ -411,9 +414,13 @@ impl StatefulWidget for PetMode<'_> {
|
||||
);
|
||||
}
|
||||
if !plan.pet.is_empty() {
|
||||
WhalePet::new(self.theme)
|
||||
.words("")
|
||||
.paint(plan.pet, buf, &mut state.stage);
|
||||
if let Some(whale) = self.dot_whale {
|
||||
whale.paint(plan.pet, buf, self.theme);
|
||||
} else {
|
||||
WhalePet::new(self.theme)
|
||||
.words("")
|
||||
.paint(plan.pet, buf, &mut state.stage);
|
||||
}
|
||||
}
|
||||
let marker = if self.theme.ascii() { "> " } else { "› " };
|
||||
let mut agent_words = self.agent_words;
|
||||
|
||||
+13
-8
@@ -57,7 +57,8 @@ impl<'a> WhalePet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Localized words from the host. The default names the current action.
|
||||
/// Localized words from the host. The default names the current action;
|
||||
/// empty words hide the caption when the host already presents the state.
|
||||
#[must_use]
|
||||
pub fn words(mut self, words: impl Into<Cow<'a, str>>) -> Self {
|
||||
self.words = Some(words.into());
|
||||
@@ -154,7 +155,9 @@ impl<'a> WhalePet<'a> {
|
||||
};
|
||||
if self.theme.ascii() || art.width < 16 || art.height < 8 {
|
||||
// A deliberately invalid frame asks the shared painter for words.
|
||||
whale.paint_frame(area, buf, self.theme, &words_only);
|
||||
if self.words.as_deref() != Some("") {
|
||||
whale.paint_frame(area, buf, self.theme, &words_only);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let dark = self.theme.caps().appearance != crate::detect::Appearance::Light;
|
||||
@@ -187,12 +190,14 @@ impl<'a> WhalePet<'a> {
|
||||
if let Ok(sprite) = Sprite::image(&pixels, usize::from(art.width), usize::from(art.width)) {
|
||||
sprite.paint(art, buf, self.theme);
|
||||
}
|
||||
whale.paint_frame(
|
||||
Rect::new(area.x, art.bottom(), area.width, 1),
|
||||
buf,
|
||||
self.theme,
|
||||
&words_only,
|
||||
);
|
||||
if self.words.as_deref() != Some("") {
|
||||
whale.paint_frame(
|
||||
Rect::new(area.x, art.bottom(), area.width, 1),
|
||||
buf,
|
||||
self.theme,
|
||||
&words_only,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -529,7 +529,11 @@ impl Whale {
|
||||
}
|
||||
let mark = StatusMark::new(self.state.state())
|
||||
.word(self.words.clone().unwrap_or_else(|| self.state.words()));
|
||||
let label = Line::from(mark.spans(theme)).centered();
|
||||
let label = if self.words.as_deref() == Some("") {
|
||||
Line::default()
|
||||
} else {
|
||||
Line::from(mark.spans(theme)).centered()
|
||||
};
|
||||
// Braille has no honest ASCII form: dot-count shading turns the whale
|
||||
// into noise. ASCII-safe terminals get the words alone.
|
||||
let grid = grid.filter(|grid| {
|
||||
|
||||
+18
-12
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::{CoveScene, Layer, Path, Role, ink::Fill, rig::holes_for, scene::segments};
|
||||
|
||||
pub(crate) const MAX_SIDE: usize = 96;
|
||||
pub(crate) const MAX_SIDE: usize = 160;
|
||||
|
||||
struct Canvas {
|
||||
side: usize,
|
||||
@@ -68,24 +68,30 @@ impl Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
fn layers(&mut self, layers: &[Layer]) {
|
||||
fn layers(&mut self, layers: &[Layer], opacity: f64, current_ink: Option<u32>) {
|
||||
for layer in layers {
|
||||
self.fill(&layer.paths, Fill::Solid(layer.color), layer.alpha);
|
||||
let color = if layer.color == 0xf7fcfb {
|
||||
current_ink.unwrap_or(layer.color)
|
||||
} else {
|
||||
layer.color
|
||||
};
|
||||
self.fill(&layer.paths, Fill::Solid(color), layer.alpha * opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Square design space, with two samples per axis for clean curved edges.
|
||||
/// Square design space, with extra samples at small sizes for clean curved edges.
|
||||
/// Public terminal rectangles never control an unbounded allocation.
|
||||
pub(crate) fn render(scene: &CoveScene, side: usize, dark: bool) -> Vec<u8> {
|
||||
if !(1..=MAX_SIDE).contains(&side) {
|
||||
return Vec::new();
|
||||
}
|
||||
let samples = if side <= 64 { 4 } else { 2 };
|
||||
let mut canvas = Canvas {
|
||||
side: side * 2,
|
||||
pixels: vec![[0.; 4]; side * side * 4],
|
||||
side: side * samples,
|
||||
pixels: vec![[0.; 4]; side * side * samples * samples],
|
||||
};
|
||||
canvas.layers(&scene.behind);
|
||||
canvas.layers(&scene.behind, if dark { 0.68 } else { 0.9 }, None);
|
||||
for shape in &scene.parts.shapes {
|
||||
if shape.role == Role::Cutout {
|
||||
continue;
|
||||
@@ -96,16 +102,16 @@ pub(crate) fn render(scene: &CoveScene, side: usize, dark: bool) -> Vec<u8> {
|
||||
canvas.fill(&paths, fill, shape.opacity);
|
||||
}
|
||||
}
|
||||
canvas.layers(&scene.front);
|
||||
canvas.layers(&scene.front, 0.82, (!dark).then_some(0x3e7789));
|
||||
let mut out = Vec::with_capacity(side * side * 4);
|
||||
for y in 0..side {
|
||||
for x in 0..side {
|
||||
let mut rgba = [0.; 4];
|
||||
for dy in 0..2 {
|
||||
for dx in 0..2 {
|
||||
let p = canvas.pixels[(y * 2 + dy) * canvas.side + x * 2 + dx];
|
||||
for dy in 0..samples {
|
||||
for dx in 0..samples {
|
||||
let p = canvas.pixels[(y * samples + dy) * canvas.side + x * samples + dx];
|
||||
for (dst, src) in rgba.iter_mut().zip(p) {
|
||||
*dst += src / 4.;
|
||||
*dst += src / (samples * samples) as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user