diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx new file mode 100644 index 0000000..9f4ebf7 --- /dev/null +++ b/src/components/common/ImageShader/index.tsx @@ -0,0 +1,444 @@ +import { forwardRef, useEffect, useRef, useState } from "react" +import { ImageSource, get_image_src } from "../../../utils/webgl/images" +import { + UniformProp, + create_program_from_strings, + get_gl_context, + get_uniform_location, + load_texture, + set_rect_vertices, + set_uniform_value, +} from "../../../utils/webgl/webgl" + +interface ImageProps { + src: string, + width?: number, + height?: number, + alt?: string, + srcSet?: ImageSource[], + className?: string, +} + +interface ImageShaderProps { + src: string, + fragSource: string, + width?: number, + height?: number, + alt?: string, + srcSet?: ImageSource[], + customUniforms?: UniformProp[], + animate?: boolean, + frameRate?: number, + className?: string, + onMouseOver?: React.MouseEventHandler, + wrapperClassName?: string, +} + +interface CustomUniformLocation { + name: string, + location: WebGLUniformLocation | null, +} + + +const ImageShader = forwardRef((props, ref) => { + // WebGl Context + let [gl, set_gl] = useState(null) + + // Shader state + let [shader_prog, set_shader_prog] = useState(null) // Shader program + + // Attribute location state + let [pos_location, set_pos_location] = useState(0) // Position attribute location + + // Uniform location state + let [res_location, set_res_location] = useState(null) // Resolution uniform + let [sampler_location, set_sampler_location] = useState(null) // Sampler uniform + let [time_location, set_time_location] = useState(null) // Time uniform + let [custom_locations, set_custom_locations] = useState([]) // Custom uniforms + + // Buffer state + let [pos_buffer, set_pos_buffer] = useState(null) // Position buffer + + // Texture state + let [texture, set_texture] = useState(null) + let [texture_width, set_texture_width] = useState(0) + let [texture_height, set_texture_height] = useState(0) + + // Renderer state + let [can_render, set_can_render] = useState(false) // Enables webgl rendering + + // Canvas dimensions + let [canvas_width, set_canvas_width] = useState(null) + let [canvas_height, set_canvas_height] = useState(null) + + // Animation interval id + let [anim_interval_id, set_anim_interval_id] = useState(null) + // Animataion tick + let [tick, set_tick] = useState(0) + + // Canvas element reference + let canvas_ref = useRef(null) + // Component wrapper element refrence + let wrapper_ref = useRef(null) + + const vert_source = ` + attribute vec3 a_position; + + varying vec2 v_texcoord; + + void main() { + gl_Position = vec4(a_position, 1.); + } + ` + + /** Set the locations for shader uniforms */ + let set_uniform_locations = ( + gl: WebGLRenderingContext, + program: WebGLProgram, + ) => { + // Set resolution uniform location + const resolution_unif_location = gl.getUniformLocation(program, "u_resolution") + set_res_location(resolution_unif_location) + + // Set texture sampler uniform location + const sampler_unif_location = gl.getUniformLocation(program, "u_texture") + set_sampler_location(sampler_unif_location) + + // Set time uniform location + const time_unif_location = gl.getUniformLocation(program, "u_time") + set_time_location(time_unif_location) + } + + /** Sets the location for custom shader uniforms */ + let set_custom_uniform_locations = ( + gl: WebGLRenderingContext, + program: WebGLProgram, + ) => { + if (props.customUniforms === undefined) return + + let update_custom_locations = [...custom_locations] + + props.customUniforms.forEach(({ name: u_name }) => { + const lidx = custom_locations.findIndex(({ name: l_name }) => l_name === u_name) + + if (lidx < 0) { + update_custom_locations.push({ + name: u_name, + location: gl.getUniformLocation(program, u_name) + }) + } + }) + + set_custom_locations(update_custom_locations) + } + + /** Set the values for shader uniforms */ + let set_uniform_values = ( + gl: WebGLRenderingContext, + ) => { + // Set resolution uniform value + gl.uniform2f(res_location, gl.canvas.width, gl.canvas.height) + + // Set sampler uniform value + gl.uniform1i(sampler_location, 0) + + // Set time uniform value + gl.uniform1f(time_location, tick) + + // Set uniform prop values + + if (props.customUniforms === undefined) return + + props.customUniforms.forEach(({ name: u_name, x, y, z, w, is_int }) => { + const lidx = custom_locations.findIndex(({ name: l_name }) => l_name === u_name) + + if (lidx >= 0) { + set_uniform_value(gl, custom_locations[lidx].location, x, y, z, w, is_int) + } + }) + } + + /** Initializes webgl and shaders */ + let init = async ( + canvas_ref: React.RefObject, + vert_source: string, + frag_source: string, + ) => { + try { + if (canvas_ref.current === null) throw "can't get canvas element" + + // Get webgl context + const ctx = get_gl_context(canvas_ref) + set_gl(ctx) + + // Create shader program + const program = create_program_from_strings(ctx, vert_source, frag_source) + set_shader_prog(program) + + // + // Get attribute locations + // + + // Get position attribute location + set_pos_location(ctx.getAttribLocation(program, "a_position")) + + // + // Set uniform locations + // + + set_uniform_locations(ctx, program) + set_custom_uniform_locations(ctx, program) + + // + // Bind buffers + // + + // Bind position buffer + const position_buffer = ctx.createBuffer(); + if (position_buffer === null) throw "can't create position buffer" + set_pos_buffer(position_buffer) + + // Create verticeis + set_rect_vertices(ctx, position_buffer) + + // Load texture + let img_src = await get_image_src(props.src, props.srcSet) + let { texture: tex, dimensions } = await load_texture(ctx, img_src) + + // Set texture state + set_texture(tex) + set_texture_width(dimensions.width) + set_texture_height(dimensions.height) + + } catch (err) { + console.error(err) + set_can_render(false) + } + } + + /** Whether webgl is initialized */ + let initialized = () => { + if (gl === null) return false + if (shader_prog === null) return false + if (pos_buffer === null) return false + if (texture === null) return false + return true + } + + /** Render webgl canvas */ + let render = () => { + if (gl === null) return + if (!initialized()) return + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Clear the canvas + gl.clearColor(0.1, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Use shader program + gl.useProgram(shader_prog); + + // + // Position attribute + // + + // Enable attribute + gl.enableVertexAttribArray(pos_location); + + // Bind the position buffer. + gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); + + // Set position data pointer + gl.vertexAttribPointer( + pos_location, + 3, // 2 components per iteration + gl.FLOAT, // 32bit float data + false, // don't normalize data + 0, // move by size * sizeof(type) each iteration + 0 // offset + ) + + // + // Texture + // + + // Use texture unit 0 + gl.activeTexture(gl.TEXTURE0) + + // Bind texture + gl.bindTexture(gl.TEXTURE_2D, texture) + + // + // Uniforms + // + + set_uniform_values(gl) + + // + // Draw + // + + gl?.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + /** Handles window resize event */ + let handle_resize = (): { width?: number, height?: number } => { + if (wrapper_ref.current === null) return {} + + let canvas_size = { + width: wrapper_ref.current.clientWidth, + height: wrapper_ref.current.clientHeight + } + + set_canvas_width(canvas_size.width) + set_canvas_height(canvas_size.height) + + return canvas_size + } + + let start_animation = (frame_rate: number = 60): NodeJS.Timeout => { + // Calculate frame delay in milliseconds + let delay = Math.round((1 / frame_rate) * 1000) + return setInterval(() => set_tick((t) => t + 1), delay) + } + + let set_animated = (animate: boolean, frame_rate?: number) => { + if (anim_interval_id !== null) { + clearInterval(anim_interval_id) + set_anim_interval_id(null) + } + + if (animate) { + const interval_id = start_animation(frame_rate) + set_anim_interval_id(interval_id) + } + + } + + /** Image fallback component */ + let Image = (props: ImageProps) => { + if (props.srcSet === undefined) { + return ( + {props.alt} + ) + } else { + return ( + + {props.srcSet.map((source, idx) => ( + + ))} + + {props.alt} + + ) + } + } + + // + // Hooks + // + + // Render when tick changes + useEffect(() => { + if (!can_render) return + render() + }, [tick]) + + // Updated can render state + useEffect(() => { + set_can_render(initialized()) + }, [ + gl, + shader_prog, + pos_location, + pos_buffer, + texture + ]) + + // Rerender when can_render state changes + useEffect(() => { + if (!can_render) return + render() + set_animated(props.animate || false, props.frameRate) + }, [can_render]) + + // Reset position buffer and rerender when canvas dimensions change + useEffect(() => { + if (gl === null || pos_buffer === null) { + return + } + + // Rebind position buffer to canvas dimensions + set_rect_vertices(gl, pos_buffer) + + // rerender + render() + + }, [canvas_width, canvas_height]) + + // Start or stop animation interval for animated prop change + useEffect(() => { + set_animated(props.animate || false, props.frameRate) + }, [props.animate, props.frameRate]) + + // Component did mount + useEffect(() => { + if (props.width !== undefined) set_canvas_width(props.width) + if (props.height !== undefined) set_canvas_height(props.height) + + window.addEventListener("resize", handle_resize) + handle_resize() + init(canvas_ref, vert_source, props.fragSource) + + return () => { + window.removeEventListener("resize", handle_resize) + + if (anim_interval_id !== null) clearInterval(anim_interval_id) + } + }, []) + + return ( +
+
+ + + + {props.alt} +
+
+ ) +}) + +export default ImageShader diff --git a/src/components/home/sections/Work/WorkCard/frag.glsl b/src/components/home/sections/Work/WorkCard/frag.glsl new file mode 100644 index 0000000..e1acbdf --- /dev/null +++ b/src/components/home/sections/Work/WorkCard/frag.glsl @@ -0,0 +1,130 @@ +precision highp float; + +uniform vec2 u_resolution; +uniform sampler2D u_texture; + +uniform float u_time; +uniform vec2 u_mouseCoord; + +// Simplex 3D Noise +// by Ian McEwan, Stefan Gustavson (https://github.com/stegu/webgl-noise) +vec4 permute(vec4 x) { + return mod(((x * 34.0) + 1.0) * x, 289.0); +} +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0. + 0.0 * C + vec3 x1 = x0 - i1 + 1.0 * C.xxx; + vec3 x2 = x0 - i2 + 2.0 * C.xxx; + vec3 x3 = x0 - 1. + 3.0 * C.xxx; + + // Permutations + i = mod(i, 289.0); + vec4 p = permute(permute(permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0)) + + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients + // ( N*N points uniformly over a square, mapped onto an octahedron.) + float n_ = 1.0 / 7.0; // N=7 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + //Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), + dot(p2, x2), dot(p3, x3))); +} + +float fractal_snoise(vec3 uv, float scale) { + float oct1 = snoise(uv * scale); + float oct2 = snoise(uv * 2. * scale); + float oct3 = snoise(uv * 4. * scale); + float oct4 = snoise(uv * 8. * scale); + + return clamp(oct1 + oct2 / 2. + oct3 / 4. + oct4 / 8., 0., 1.); +} + +vec2 offset_by_dir(vec2 v, vec2 dir, float value) { + v = v - dir * value; + v.x = clamp(v.x, 0., 1.); + v.y = clamp(v.y, 0., 1.); + return v; +} + +void main() +{ + // Noramlize frag coords from 0. to 1. + vec2 uv = gl_FragCoord.xy / u_resolution.xy; + uv.y = 1. - uv.y; // Flip coords verticaly + + // Get distance from mouse coords + vec2 uv2 = (vec2(gl_FragCoord.x, 1. - gl_FragCoord.y) - 0.5 * vec2(u_resolution.x, 1. - u_resolution.y)) / u_resolution.y; + vec2 center = (u_mouseCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y; + float dist = length(center - uv2) - 0.; + + // Get texture uv offset + float height = fractal_snoise(vec3(uv, u_time * 0.01) + vec3(center, 0.), 1.); + float value = clamp(height * (1. - dist * 2.), 0., 2.); + vec2 dir = vec2(0.5 - uv.x, 0.5 - uv.y); + + // Offset texture uvs by different amounts for each color channel + vec2 tuvr = offset_by_dir(uv, dir, value * 0.33); + vec2 tuvg = offset_by_dir(uv, dir, value * 0.66); + vec2 tuvb = offset_by_dir(uv, dir, value); + + // Sample texture + float texr = texture2D(u_texture, tuvr).r; + float texg = texture2D(u_texture, tuvg).g; + float texb = texture2D(u_texture, tuvb).b; + + gl_FragColor = vec4(texr, texg, texb, 1.); +} diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index 9786f03..2e9432e 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -1,10 +1,13 @@ -import { FunctionComponent } from "react"; +import { FunctionComponent, useEffect, useRef, useState } from "react"; import { addLeadingZeros } from "../../../../../utils/numberFormatting"; import { Work } from "../../../../../types"; import SkillIcon from "../../../../common/SkillIcon"; import { Link } from "react-router-dom"; +import ImageShader from "../../../../common/ImageShader"; + +import frag_source from "./frag.glsl?raw" interface WorkCardProps { work: Work; @@ -13,12 +16,65 @@ interface WorkCardProps { /** Work section work card component */ const WorkCard: FunctionComponent = (props) => { + const [mouse_x, set_mouse_x] = useState(-1000) + const [mouse_y, set_mouse_y] = useState(-1000) + + const image_shader_ref = useRef(null) + const image_flip_classes = props.flipped ? "col-start-2 col-end-4" : "col-span-2"; const work_link = (id: string) => `/work/${id.toLowerCase()}`; + const handle_mouse_move = (event: MouseEvent) => { + if (image_shader_ref.current === null) { + set_mouse_x(0) + set_mouse_y(0) + return + } + + const image_rect = image_shader_ref.current.getBoundingClientRect() + + const x = event.clientX - image_rect.left + const y = event.clientY - image_rect.top + + set_mouse_x(x) + set_mouse_y(y) + } + + const frag_source_old = ` + precision highp float; + + uniform vec2 u_resolution; + uniform sampler2D u_texture; + + uniform float u_time; + uniform vec2 u_mouseCoord; + + void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + vec2 muv = u_mouseCoord / u_resolution; + muv.x = muv.x * -1.; + // uv = uv + muv; + + vec4 tex_col = texture2D(u_texture, vec2(uv.x, 1. - uv.y)); + + vec3 col = 0.5 + 0.5*cos(u_time * 0.01 + uv.xyx + vec3(0,2,4)); + + // gl_FragColor = tex_col; + gl_FragColor = vec4(col, 1.); + }` + + // Component did mount + useEffect(() => { + window.addEventListener("mousemove", handle_mouse_move) + + return () => { + window.removeEventListener("mousemove", handle_mouse_move) + } + }, []) + return (
= (props) => { to={work_link(props.work.id)} className="block aspect-[16/9] xl:aspect-auto w-full h-full overflow-hidden border border-brand-gray-400 rounded" > - - - - - - - - {`${props.work.title}`} - +
diff --git a/src/utils/hooks/useUniform.ts b/src/utils/hooks/useUniform.ts new file mode 100644 index 0000000..238e69d --- /dev/null +++ b/src/utils/hooks/useUniform.ts @@ -0,0 +1,156 @@ +import { useCallback, useState } from "react" + +export type UseUniformHook = { + /** Sets values of the uniform */ + setValues: UniformValueSetter + /** Sets location state to uniforms state in a program */ + setLocation: UniformLocationSetter, + /** Sets uniform value to set values */ + setUniform: UniformSetter, +} + +type UniformLocationSetter = (gl: WebGLRenderingContext, program: WebGLProgram) => void; +type UniformSetter = (gl: WebGLRenderingContext) => void; +type UniformValueSetter = { + (x: number): void, + (x: number, y: number): void, + (x: number, y: number, z: number): void, + (x: number, y: number, z: number, w: number): void, + (v: Float32List | Int32List | number[]): void, +} + +/** WebGL uniform helper hook */ +export function useUniform(name: string, x: number): UseUniformHook +export function useUniform(name: string, x: number, y: number): UseUniformHook +export function useUniform(name: string, x: number, y: number): UseUniformHook +export function useUniform(name: string, x: number, y: number, z: number): UseUniformHook +export function useUniform(name: string, x: number, y: number, z: number, w: number): UseUniformHook +export function useUniform(name: string, v: Float32List | Int32List | number[]): UseUniformHook +export function useUniform( + name: string, + x: number | Float32List | Int32List | number[], + y?: number, + z?: number, + w?: number, +): UseUniformHook { + // Uniform name + const [u_name, set_u_name] = useState(name) + // Determins whether uniform is an int + const [is_int, set_is_int] = useState(false) + // Location of uniform + const [u_location, set_u_location] = useState(null) + + // Value(s) + const [u_x, set_u_x] = useState(typeof x === "number" ? x : undefined) + const [u_y, set_u_y] = useState(y) + const [u_z, set_u_z] = useState(z) + const [u_w, set_u_w] = useState(w) + const [u_v, set_u_v] = useState( + typeof x === "number" ? undefined : x + ) + + const setValues: UniformValueSetter = ( + x: number | Float32List | Int32List | number[], + y?: number, + z?: number, + w?: number, + ) => { + if (typeof x !== "number") { + set_u_v(x) + return + } + + set_u_x(x) + set_u_y(y) + set_u_z(z) + set_u_w(w) + } + + const setLocation: UniformLocationSetter = (gl, program) => { + const location = gl.getUniformLocation(program, u_name) + + if (location === null) { + set_u_location(null) + throw `unable to get location for uniform ${u_name}` + } + + set_u_location(location) + } + + /** Set uniform in a webgl rendering context to values */ + const setUniform: UniformSetter = useCallback((gl) => { + console.log(u_x, u_y) + // Values supplied as iterable + if (u_v !== undefined) { + // Int values + if (u_v instanceof Int32Array) { + switch (u_v.length) { + case 1: + gl.uniform1iv(u_location, u_v) + break; + case 2: + gl.uniform2iv(u_location, u_v) + break; + case 3: + gl.uniform3iv(u_location, u_v) + break; + default: + gl.uniform4iv(u_location, u_v) + break; + } + + return + + // Float values + } else { + switch (u_v.length) { + case 1: + gl.uniform1fv(u_location, u_v) + break; + case 2: + gl.uniform2fv(u_location, u_v) + break; + case 3: + gl.uniform3fv(u_location, u_v) + break; + default: + gl.uniform4fv(u_location, u_v) + break; + } + + return + } + + } else { + if (u_x !== undefined && u_y !== undefined && u_z !== undefined && u_w !== undefined) { + is_int ? + gl.uniform4i(u_location, u_x, u_y, u_z, u_w) : + gl.uniform4f(u_location, u_x, u_y, u_z, u_w) + return + } + + if (u_x !== undefined && u_y !== undefined && u_z !== undefined) { + is_int ? + gl.uniform3i(u_location, u_x, u_y, u_z) : + gl.uniform3f(u_location, u_x, u_y, u_z) + return + } + + if (u_x !== undefined && u_y !== undefined) { + is_int ? gl.uniform2i(u_location, u_x, u_y) : gl.uniform2f(u_location, u_x, u_y) + return + } + + if (u_x !== undefined) { + is_int ? gl.uniform1i(u_location, u_x) : gl.uniform1f(u_location, u_x) + return + } + } + }, [u_location, u_x, u_y, u_z, u_w, u_v]) + + return { + setValues, + setLocation, + setUniform, + } +} diff --git a/src/utils/webgl/images.ts b/src/utils/webgl/images.ts new file mode 100644 index 0000000..ad5bd51 --- /dev/null +++ b/src/utils/webgl/images.ts @@ -0,0 +1,91 @@ +/** Image helper functions */ + +/** Image source object */ +export interface ImageSource { + media?: string, // Image media query + srcSet: string, // Image src string + type: string, // Image type +} + +/** Webp image features */ +export enum WebpFeature { + LOSSY = "lossy", + LOSSLESS = "lossless", + ALPHA = "alpha", + ANIMATION = "animation" +} + +/** + * Checks whether a webp feature is supported. + * Adapted from: https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp +*/ +export function check_webp_feature( + feature: WebpFeature, +): Promise { + return new Promise((resolve, reject) => { + var kTestImages = { + lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA", + lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==", + alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==", + animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA" + }; + + var img = document.createElement("img") + + img.onload = function() { + var result = (img.width > 0) && (img.height > 0); + resolve(result) + }; + + img.onerror = function() { + reject() + }; + + img.src = "data:image/webp;base64," + kTestImages[feature]; + }) +} + +/** Get the appropriate image source provided a source string and an optional image source array */ +export async function get_image_src(src: string, src_set?: ImageSource[]): Promise { + if (src_set === undefined || src_set.length <= 0) { + return src + } + + // Check if browser supports relevant webp features + let supports_webp = await (async (): Promise => { + try { + let supports_lossless = await check_webp_feature(WebpFeature.LOSSLESS) + let supports_lossy = await check_webp_feature(WebpFeature.LOSSY) + let supports_alpha = await check_webp_feature(WebpFeature.ALPHA) + + return (supports_lossless && supports_lossy && supports_alpha) + } catch (_) { + return false + } + })() + + // Split png and webp sources into seperate arrays + let png_set = src_set.filter(({ type }) => type === "image/png") + let webp_set = src_set.filter(({ type }) => type === "image/webp") + + // Tests if an ImageSource matches a media query + let match_src = (img_src: ImageSource): Boolean => { + if (img_src.media === undefined) return true + return window.matchMedia(img_src.media).matches + } + + + if ((!supports_webp || webp_set.length <= 0) && png_set.length > 0) { + // Use png srcSet + let result = png_set.find(match_src) + if (result !== undefined) return result.srcSet + + } else if (supports_webp && webp_set.length > 0) { + // Use webp srcSet + let result = webp_set.find(match_src) + if (result !== undefined) return result.srcSet + } + + // Use src if no image sources match + return src +} diff --git a/src/utils/webgl/webgl.ts b/src/utils/webgl/webgl.ts new file mode 100644 index 0000000..2316909 --- /dev/null +++ b/src/utils/webgl/webgl.ts @@ -0,0 +1,240 @@ +/** WebGL helper functions */ + +export interface UniformProp { + name: string, + x: number, + y?: number, + z?: number, + w?: number, + is_int?: Boolean, +} + +/** Get the location of a WebGL uniform in a webgl program by name */ +export function get_uniform_location( + gl: WebGLRenderingContext, + program: WebGLProgram, + name: string): WebGLUniformLocation | null { + return gl.getUniformLocation(program, name) +} + +/** Set a WebGL uniforms value at a uniform location */ +export function set_uniform_value( + gl: WebGLRenderingContext, + location: WebGLUniformLocation | null, + x: number, + y?: number, + z?: number, + w?: number, + is_int: Boolean = false, +): void { + if (y !== undefined && z !== undefined && w !== undefined) { + is_int ? gl.uniform4i(location, x, y, z, w) : gl.uniform4f(location, x, y, z, w) + return + } + + if (y !== undefined && z !== undefined) { + is_int ? gl.uniform3i(location, x, y, z) : gl.uniform3f(location, x, y, z) + return + } + + if (y !== undefined) { + is_int ? gl.uniform2i(location, x, y) : gl.uniform2f(location, x, y) + return + } + + is_int ? gl.uniform1i(location, x) : gl.uniform1f(location, x) + return +} + +/** Get a webgl context for a canvas ref */ +export function get_gl_context( + canvas: React.RefObject +): WebGLRenderingContext { + if (canvas.current === null) throw "canvas element is null" + + const ctx = canvas.current.getContext("webgl") + + if (ctx === null) throw "failed to get webgl context" + + return ctx +} + +/** + * Creates a webgl shader from a source string + * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html +*/ +export function create_shader( + gl: WebGLRenderingContext, + type: number, + source: string +): WebGLShader { + const shader = gl.createShader(type); + + if (shader === null) { + throw "failed to create shader" + } + + gl.shaderSource(shader, source) + gl.compileShader(shader) + + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS) + if (!success) { + let err = gl.getShaderInfoLog(shader) + gl.deleteShader(shader) + throw err || "failed to compile shader" + } + + return shader; +} + +/** + * Creates a shader program from a vertex shader and a fragment shader + * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html + */ +export function create_program( + gl: WebGLRenderingContext, + vertex_shader: WebGLShader, + fragment_shader: WebGLShader +): WebGLProgram { + const program = gl.createProgram(); + + if (program === null) { + throw "failed to create shader program" + } + + gl.attachShader(program, vertex_shader) + gl.attachShader(program, fragment_shader) + gl.linkProgram(program) + + const success = gl.getProgramParameter(program, gl.LINK_STATUS) + if (!success) { + let err = gl.getProgramInfoLog(program) + gl.deleteProgram(program) + throw err || "failed to link shader program" + } + + return program; +} + +/** Creates a shader program from a vertex shader source string and a fragment shader source string */ +export function create_program_from_strings( + gl: WebGLRenderingContext, + vertex_shader_src: string, + fragment_shader_src: string +): WebGLProgram { + // Create shaders from sources + const vert_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_shader_src) + const frag_shader = create_shader(gl, gl.FRAGMENT_SHADER, fragment_shader_src) + + // Create program from shaders + const program = create_program(gl, vert_shader, frag_shader) + + return program +} + +/** Bind and fill a buffer with position data for a rectangle */ +export function set_rect_vertices( + gl: WebGLRenderingContext, + buffer: WebGLBuffer, +) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer) + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + 1.0, 1.0, + 0.0, -1.0, + 1.0, 0.0, + 1.0, -1.0, + 0.0, -1.0, + -1.0, 0.0 + ]), + gl.STATIC_DRAW + ); +} + +/** Bind and fill a buffer with texture coordinates for a rectangle */ +export function set_rect_texcoords(gl: WebGLRenderingContext, buffer: WebGLBuffer) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer) + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + 0, 0, // top left + 1, 0, // top right + 1, 1, // bottom right + 1, 1, // bottom right + 0, 1, // bottom left + 0, 0 // top left + ]), + gl.STATIC_DRAW) +} + +type TextureData = { + texture: WebGLTexture, + dimensions: { width: number, height: number } +} + +/** Loads an image from a src and binds it to a webgl texture */ +export function load_texture( + gl: WebGLRenderingContext, + src: string +): Promise { + return new Promise((resolve, reject) => { + // Create image element + const image = document.createElement("img") + + // On image loaded + image.addEventListener("load", () => { + // Create texture. + const texture = gl.createTexture() + + if (texture === null) { + reject("Failed to create texture") + return + } + + gl.bindTexture(gl.TEXTURE_2D, texture) + + // Fill texture with a 1x1 blue pixel + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 1, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255]) + ) + + gl.bindTexture(gl.TEXTURE_2D, texture) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) + + // Handle power of 2 images and non power of 2 images + if (is_power_of_2(image.width) && is_power_of_2(image.height)) { + // Generate mip maps + gl.generateMipmap(gl.TEXTURE_2D); + } else { + // Turn off mipmaps clamp image to edge + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + } + + resolve({ texture, dimensions: { width: image.width, height: image.height } }) + }) + + // On image load failed + image.addEventListener("error", (e) => { + reject(e.message) + }) + + // Set image source + image.src = src + }) +} + +/** Is a number of 2 */ +function is_power_of_2(value: number): boolean { + return (value & (value - 1)) === 0; +}