use macroquad::prelude::*;
const CURVE: f32 = 0.02;
pub fn warp(p: Vec2) -> Vec2 {
let c = p * 2. - 1.;
let c = c * vec2(1. + CURVE * c.y * c.y, 1. + CURVE * c.x * c.x);
c * 0.5 + 0.5
}
pub fn lamp_light(p: Vec2, time: f32) -> f32 {
let d = (p - vec2(240., 95.)) / vec2(250., 160.);
let breathe = 0.5 + 0.5 * (time * std::f32::consts::TAU / 8.).sin();
(-d.dot(d) * 1.8).exp() * (0.55 + 0.45 * breathe)
}
const VERTEX: &str = "#version 100
attribute vec3 position;
attribute vec2 texcoord;
varying lowp vec2 uv;
uniform mat4 Model;
uniform mat4 Projection;
void main() {
gl_Position = Projection * Model * vec4(position, 1);
uv = texcoord;
}
";
const SCREEN: &str = "#version 100
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
varying vec2 uv;
uniform sampler2D Texture;
uniform vec2 src_size;
uniform vec2 dst_size;
uniform vec3 border;
uniform float time;
const float CURVE = 0.02;
vec2 warp(vec2 p) {
vec2 c = p * 2.0 - 1.0;
c *= vec2(1.0 + CURVE * c.y * c.y, 1.0 + CURVE * c.x * c.x);
return c * 0.5 + 0.5;
}
// Crisp pixels (\"sharp bilinear\"): each game pixel is drawn solid, and only the
// screen pixel that straddles its edge is blended, so curved lines don't get
// jagged. `d` is the offset from the game pixel's centre; inside `r` it stays
// on the centre, and in the last screen pixel before the edge it slides
// towards the neighbour.
vec3 tex(vec2 p) {
vec2 t = p * src_size;
vec2 scale = dst_size / src_size;
vec2 d = fract(t) - 0.5;
vec2 r = 0.5 - 0.5 / scale;
vec2 f = (d - clamp(d, -r, r)) * scale + 0.5;
return texture2D(Texture, (floor(t) + f) / src_size).rgb;
}
void main() {
vec2 p = warp(uv);
// No colour fringing or glow: both smear the small pixel text.
vec3 col = tex(p);
// Scanlines, one per game pixel row; they fade out when the game is drawn
// too small for them to look right.
float s = dst_size.y / src_size.y;
float row = fract(p.y * src_size.y) - 0.5;
col *= 1.0 + smoothstep(1.5, 3.0, s) * (0.02 - 0.12 * row * row * 4.0);
// A little darker in the far corners only, where nothing is printed.
vec2 v = p * (1.0 - p);
col *= mix(0.88, 1.0, pow(16.0 * v.x * v.y, 0.1));
// Purple in the shadows, a little warmth in the highlights.
float l = dot(col, vec3(0.299, 0.587, 0.114));
col += vec3(0.03, 0.0, 0.05) * (1.0 - l) * (1.0 - l);
col *= mix(vec3(1.0), vec3(1.04, 1.0, 0.95), l);
// Very faint grain, changing a few times a second.
vec2 cell = floor(uv * dst_size / 2.0);
float n = fract(sin(dot(cell, vec2(12.9898, 78.233)) + floor(time * 8.0) * 7.31) * 43758.5453);
col += (n - 0.5) * 0.012;
// Rounded edges of the screen, smoothed over one screen pixel.
vec2 e = min(p, 1.0 - p) * dst_size;
float inside = clamp(min(e.x, e.y), 0.0, 1.0);
gl_FragColor = vec4(mix(border, col, inside), 1.0);
}
";
const LAMP: &str = "#version 100
precision mediump float;
varying vec2 uv;
uniform sampler2D Texture;
uniform vec2 src_size;
uniform float time;
uniform float mood;
// Ordered dither: a fixed 4x4 pattern of thresholds.
float bayer2(vec2 a) {
a = floor(a);
return fract(a.x / 2.0 + a.y * a.y * 0.75);
}
float bayer4(vec2 a) {
return bayer2(0.5 * a) * 0.25 + bayer2(a);
}
void main() {
vec3 col = texture2D(Texture, uv).rgb;
vec2 px = floor(uv * src_size);
// A lamp over the middle of the table that breathes, once every 8 s.
vec2 d = (px - vec2(240.0, 95.0)) / vec2(250.0, 160.0);
float breathe = 0.5 + 0.5 * sin(time * 6.2831853 / 8.0);
float light = exp(-dot(d, d) * 1.8) * (0.55 + 0.45 * breathe);
// Light in a few steps, dithered, so it stays pixel art.
light = floor(light * 8.0 + bayer4(px)) / 8.0;
col *= 0.8 + 0.3 * light;
col += vec3(0.04, 0.03, 0.0) * light;
// Warmer and a touch brighter after a win, cooler after a loss.
col += light * (max(mood, 0.0) * vec3(0.05, 0.025, -0.01) + max(-mood, 0.0) * vec3(-0.025, 0.0, 0.06));
gl_FragColor = vec4(col, 1.0);
}
";
const SHADE_VERTEX: &str = "#version 100
attribute vec3 position;
attribute vec2 texcoord;
attribute vec4 color0;
varying lowp vec2 uv;
varying lowp vec4 color;
uniform mat4 Model;
uniform mat4 Projection;
void main() {
gl_Position = Projection * Model * vec4(position, 1);
gl_Position.z = -0.9 * gl_Position.w;
uv = texcoord;
color = color0 / 255.0;
}
";
const COVER_VERTEX: &str = "#version 100
attribute vec3 position;
attribute vec2 texcoord;
attribute vec4 color0;
varying lowp vec2 uv;
varying lowp vec4 color;
uniform mat4 Model;
uniform mat4 Projection;
void main() {
gl_Position = Projection * Model * vec4(position, 1);
gl_Position.z = 0.9 * gl_Position.w;
uv = texcoord;
color = color0 / 255.0;
}
";
const FLAT: &str = "#version 100
precision lowp float;
varying lowp vec2 uv;
varying lowp vec4 color;
uniform sampler2D Texture;
void main() {
vec4 c = texture2D(Texture, uv) * color;
if (c.a < 0.01) discard;
gl_FragColor = c;
}
";
fn layer(vertex: &str, depth_test: Comparison) -> Option<Material> {
let pipeline_params = PipelineParams {
depth_test,
depth_write: true,
color_blend: Some(miniquad::BlendState::new(
miniquad::Equation::Add,
miniquad::BlendFactor::Value(miniquad::BlendValue::SourceAlpha),
miniquad::BlendFactor::OneMinusValue(miniquad::BlendValue::SourceAlpha),
)),
..Default::default()
};
load_material(ShaderSource::Glsl { vertex, fragment: FLAT }, MaterialParams { pipeline_params, ..Default::default() })
.inspect_err(|e| eprintln!("hushjack: shader not available, drawing without it: {e:?}"))
.ok()
}
pub fn shade() -> Option<Material> {
layer(SHADE_VERTEX, Comparison::Less)
}
pub fn cover() -> Option<Material> {
layer(COVER_VERTEX, Comparison::Always)
}
fn load(fragment: &str, uniforms: &[(&str, UniformType)]) -> Option<Material> {
let uniforms = uniforms.iter().map(|&(n, t)| UniformDesc::new(n, t)).collect();
let shader = ShaderSource::Glsl { vertex: VERTEX, fragment };
load_material(shader, MaterialParams { uniforms, ..Default::default() })
.inspect_err(|e| eprintln!("hushjack: shader not available, drawing without it: {e:?}"))
.ok()
}
pub fn screen() -> Option<Material> {
load(
SCREEN,
&[
("src_size", UniformType::Float2),
("dst_size", UniformType::Float2),
("border", UniformType::Float3),
("time", UniformType::Float1),
],
)
}
pub fn lamp() -> Option<Material> {
load(LAMP, &[("src_size", UniformType::Float2), ("time", UniformType::Float1), ("mood", UniformType::Float1)])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn warp_keeps_the_middle_and_bends_the_corners_out() {
assert_eq!(warp(vec2(0.5, 0.5)), vec2(0.5, 0.5));
assert!((warp(vec2(0., 0.5)) - vec2(0., 0.5)).length() < 1e-6);
assert!((warp(vec2(1., 0.5)) - vec2(1., 0.5)).length() < 1e-6);
let c = warp(vec2(1., 1.));
assert!(c.x > 1. && c.y > 1.);
let (a, b) = (warp(vec2(0.2, 0.3)), warp(vec2(0.8, 0.7)));
assert!((a + b - vec2(1., 1.)).length() < 1e-6);
}
}