your personal website on atproto - mirror
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #2 from unbedenklich/main

main to text fluid

authored by

David Terterian and committed by
GitHub
1e11c704 b749e33b

+3807 -6
+12
docs/Autofill.md
··· 1 + # autofilling a profile 2 + 3 + - find all links in bluesky profile 4 + - check for lexicons: 5 + - place.stream.livestream 6 + - social.popfeed.challenge.participation 7 + - social.popfeed.feed.review 8 + - site.standard.document 9 + - com.germnetwork.declaration 10 + - pub.leaflet.document 11 + - blue.flashes.actor.portfolio 12 + - add bluesky profile card
+5 -1
docs/CardIdeas.md
··· 39 39 - teal.fm 40 40 - tangled.sh 41 41 - popfeed.social 42 + - reading goal 43 + - latest ratings 44 + - lists 42 45 - smokesignal.events (https://pdsls.dev/at://did:plc:xbtmt2zjwlrfegqvch7fboei/events.smokesignal.calendar.event/3ltn2qrxf3626) 43 46 - statusphere.xyz 44 47 - goals.garden 45 48 - flashes.blue (https://pdsls.dev/at://did:plc:aytgljyikzbtgrnac2u4ccft/blue.flashes.actor.portfolio, https://app.flashes.blue/profile/j4ck.xyz) 46 49 - room: flo-bit.dev/room 50 + - plyr.fm 47 51 48 52 ## programming 49 53 ··· 58 62 - minesweeper 59 63 - memory 60 64 - pinball esque 61 - - 2048 65 + - 2048
+1 -1
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 159 159 160 160 function applyChange(def: (typeof AllCardDefinitions)[number]) { 161 161 const updated = def.change ? def.change(item) : item; 162 - if (updated !== item) { 162 + if (updated && updated !== item) { 163 163 item = updated; 164 164 } 165 165 item.cardType = def.type;
+26 -1
src/lib/cards/BigSocialCard/index.ts
··· 143 143 line: /(?:line\.me)/i, 144 144 skype: /(?:skype\.com)/i, 145 145 146 - ao3: /(?:archiveofourown\.org)/i 146 + ao3: /(?:archiveofourown\.org)/i, 147 + 148 + germ: /(?:ger\.mx)/i 147 149 }; 148 150 149 151 export const platformsData: Record<string, SimpleIcon> = { ··· 180 182 source: 'https://brand.linkedin.com', 181 183 guidelines: 'https://brand.linkedin.com/policies', 182 184 svg: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LinkedIn</title><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>` 185 + }, 186 + 187 + germ: { 188 + slug: 'germ', 189 + path: '', 190 + title: 'Germ Network', 191 + hex: '7EE35A', 192 + source: '', 193 + svg: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 194 + <g clip-path="url(#clip0_2_3)"> 195 + <path d="M9.97852 2.31976C10.9816 4.81045 13.9828 5.28448 15.4834 3.19552C15.6921 2.89824 15.9489 2.6572 16.0371 2.6572C16.3501 2.6572 18.2841 4.7301 18.6933 5.50945C19.2791 6.62624 19.247 6.65838 18.1476 6.32093C15.018 5.34876 12.4742 6.21648 11.1742 8.69111C10.2112 10.539 11.1822 13.4475 13.0841 14.3956C14.1674 14.9339 14.7933 14.8054 16.2378 13.7207C18.0513 12.3629 19.0303 13.1342 17.626 14.8134C15.652 17.1836 12.3378 18.2039 9.32852 17.3764C8.21309 17.0711 8.11679 16.9506 8.20506 15.9623C8.35753 14.3956 7.77173 12.7405 6.63223 11.5112C5.68532 10.4908 5.75754 10.2739 7.2421 9.7838C10.797 8.59469 10.8933 4.15162 7.38655 3.17945C6.99334 3.06696 6.6884 2.92234 6.71247 2.85807C6.79272 2.62507 9.08778 1.50024 9.56926 1.46007C9.60136 1.46007 9.78592 1.84572 9.97852 2.31976Z" fill="#4183FF"/> 196 + <path d="M18.1316 7.74304C19.7445 8.54648 19.9773 9.14104 19.4958 11.1979L19.4935 11.2075C19.255 12.2183 19.2534 12.2252 18.7415 11.8728C17.8587 11.2782 17.0563 11.463 15.5316 12.6039C14.2637 13.56 13.8625 13.568 12.9797 12.6441C10.1791 9.71149 14.2637 5.83083 18.1316 7.74304Z" fill="#22F137"/> 197 + <path d="M6.50383 4.35248C8.08469 4.64976 8.71864 5.3809 8.55012 6.71462C8.42975 7.65466 8.03654 8.05638 6.6563 8.62683C4.30507 9.599 4.04026 10.5631 5.62914 12.3789C6.45568 13.327 6.82482 14.1706 6.89704 15.2714C6.96926 16.4122 6.88902 16.4122 5.66927 15.1589C3.20569 12.6521 2.61989 9.15711 4.13655 5.96741C4.96309 4.24 5.15569 4.10341 6.50383 4.35248Z" fill="#22F137"/> 198 + <path d="M13.7581 1.58862C14.7371 1.89393 14.681 1.83769 14.4483 2.28762C13.6618 3.80614 11.1341 3.15534 11.1341 1.43596C11.1341 1.10655 12.4341 1.18689 13.7581 1.58862Z" fill="#22F137"/> 199 + <path fill-rule="evenodd" clip-rule="evenodd" d="M9.40876 0.19865C2.66804 1.65289 -0.260969 9.79987 3.97606 15.3115C8.39765 21.0723 17.6983 19.7867 20.3143 13.0378C23.1069 5.83083 16.952 -1.41628 9.40876 0.19865ZM14.4483 2.28762C14.681 1.83769 14.7371 1.89393 13.7581 1.58862C12.4341 1.18689 11.1341 1.10655 11.1341 1.43596C11.1341 3.15534 13.6618 3.80614 14.4483 2.28762ZM15.4834 3.19552C13.9828 5.28448 10.9816 4.81045 9.97852 2.31976C9.78592 1.84572 9.60136 1.46007 9.56926 1.46007C9.08778 1.50024 6.79272 2.62507 6.71247 2.85807C6.6884 2.92234 6.99334 3.06696 7.38655 3.17945C10.8933 4.15162 10.797 8.59469 7.2421 9.7838C5.75754 10.2739 5.68532 10.4908 6.63223 11.5112C7.77173 12.7405 8.35753 14.3956 8.20506 15.9623C8.11679 16.9506 8.21309 17.0711 9.32852 17.3764C12.3378 18.2039 15.652 17.1836 17.626 14.8134C19.0303 13.1342 18.0513 12.3629 16.2378 13.7207C14.7933 14.8054 14.1674 14.9339 13.0841 14.3956C11.1822 13.4475 10.2112 10.539 11.1742 8.69111C12.4742 6.21648 15.018 5.34876 18.1476 6.32093C19.247 6.65838 19.2791 6.62624 18.6933 5.50945C18.2841 4.7301 16.3501 2.6572 16.0371 2.6572C15.9489 2.6572 15.6921 2.89824 15.4834 3.19552ZM8.55012 6.71462C8.71864 5.3809 8.08469 4.64976 6.50383 4.35248C5.15569 4.10341 4.96309 4.24 4.13655 5.96741C2.61989 9.15711 3.20569 12.6521 5.66927 15.1589C6.88902 16.4122 6.96926 16.4122 6.89704 15.2714C6.82482 14.1706 6.45568 13.327 5.62914 12.3789C4.04026 10.5631 4.30507 9.599 6.6563 8.62683C8.03654 8.05638 8.42975 7.65466 8.55012 6.71462ZM19.4958 11.1979C19.9773 9.14104 19.7445 8.54648 18.1316 7.74304C14.2637 5.83083 10.1791 9.71149 12.9797 12.6441C13.8625 13.568 14.2637 13.56 15.5316 12.6039C17.0563 11.463 17.8587 11.2782 18.7415 11.8728C19.2534 12.2252 19.255 12.2183 19.4935 11.2075L19.4958 11.1979Z" fill="black"/> 200 + <path d="M11.11 21.3936C11.0939 21.4097 10.5402 21.4579 9.89025 21.4981C7.15383 21.6668 5.5489 22.5586 6.71247 23.2657C8.25321 24.2057 14.7371 24.2057 16.2779 23.2657C17.3853 22.5988 15.9248 21.707 13.4211 21.5222C11.4711 21.3776 11.1501 21.3615 11.11 21.3936Z" fill="black"/> 201 + </g> 202 + <defs> 203 + <clipPath id="clip0_2_3"> 204 + <rect width="24" height="24" fill="white"/> 205 + </clipPath> 206 + </defs> 207 + </svg>` 183 208 }, 184 209 185 210 // support / monetization
+34
src/lib/cards/FluidTextCard/CreateFluidTextCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Button, Input, Modal, Subheading, Label } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + 5 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 + 7 + let text = $state(item.cardData?.text || ''); 8 + 9 + function handleCreate() { 10 + if (!text.trim()) return; 11 + item.cardData.text = text.trim(); 12 + oncreate(); 13 + } 14 + </script> 15 + 16 + <Modal open={true} closeButton={false}> 17 + <Subheading>Enter text for fluid effect</Subheading> 18 + <div class="mt-2"> 19 + <Label class="mb-1 text-xs">Text</Label> 20 + <Input 21 + bind:value={text} 22 + placeholder="Enter your text..." 23 + autofocus 24 + onkeydown={(e) => { 25 + if (e.key === 'Enter') handleCreate(); 26 + }} 27 + /> 28 + </div> 29 + 30 + <div class="mt-4 flex justify-end gap-2"> 31 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 32 + <Button onclick={handleCreate} disabled={!text.trim()}>Create</Button> 33 + </div> 34 + </Modal>
+1515
src/lib/cards/FluidTextCard/FluidTextCard.svelte
··· 1 + <script lang="ts"> 2 + import type { ContentComponentProps } from '../types'; 3 + import { onMount, onDestroy, tick } from 'svelte'; 4 + import ditheringTextureUrl from './text_effect_fluid-main/LDR_LLL1_0.png'; 5 + 6 + let { item }: ContentComponentProps = $props(); 7 + 8 + let container: HTMLDivElement; 9 + let fluidCanvas: HTMLCanvasElement; 10 + let maskCanvas: HTMLCanvasElement; 11 + let animationId: number; 12 + let splatIntervalId: ReturnType<typeof setInterval>; 13 + let isInitialized = $state(false); 14 + let resizeObserver: ResizeObserver | null = null; 15 + 16 + // Get text from card data 17 + const text = $derived((item.cardData?.text as string) || 'hello'); 18 + const fontWeight = $derived((item.cardData?.fontWeight as string) || '900'); 19 + const fontFamily = $derived((item.cardData?.fontFamily as string) || 'Arial'); 20 + const fontSize = $derived((item.cardData?.fontSize as number) || 0.33); 21 + 22 + // Draw text mask on overlay canvas 23 + function drawOverlayCanvas() { 24 + if (!maskCanvas || !container) return; 25 + 26 + const rect = container.getBoundingClientRect(); 27 + if (rect.width === 0 || rect.height === 0) return; 28 + 29 + const dpr = window.devicePixelRatio || 1; 30 + 31 + maskCanvas.width = rect.width * dpr; 32 + maskCanvas.height = rect.height * dpr; 33 + 34 + const ctx = maskCanvas.getContext('2d')!; 35 + ctx.scale(dpr, dpr); 36 + 37 + ctx.fillStyle = 'black'; 38 + ctx.fillRect(0, 0, rect.width, rect.height); 39 + 40 + const textFontSize = Math.round(rect.width * fontSize); 41 + ctx.font = fontWeight + ' ' + textFontSize + 'px ' + fontFamily; 42 + 43 + ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; 44 + ctx.lineWidth = 2; 45 + ctx.textBaseline = 'middle'; 46 + ctx.textAlign = 'center'; 47 + 48 + ctx.strokeText(text, rect.width / 2, rect.height / 2); 49 + ctx.globalCompositeOperation = 'destination-out'; 50 + ctx.fillText(text, rect.width / 2, rect.height / 2); 51 + } 52 + 53 + // Redraw overlay when text settings change (only after initialization) 54 + $effect(() => { 55 + // Access all reactive values to track them 56 + text; 57 + fontWeight; 58 + fontFamily; 59 + fontSize; 60 + // Only redraw if already initialized 61 + if (isInitialized) { 62 + drawOverlayCanvas(); 63 + } 64 + }); 65 + 66 + onMount(async () => { 67 + // Wait for layout to settle 68 + await tick(); 69 + initFluidSimulation(); 70 + }); 71 + 72 + onDestroy(() => { 73 + if (animationId) cancelAnimationFrame(animationId); 74 + if (splatIntervalId) clearInterval(splatIntervalId); 75 + if (resizeObserver) resizeObserver.disconnect(); 76 + }); 77 + 78 + function initFluidSimulation() { 79 + if (!fluidCanvas || !maskCanvas || !container) return; 80 + 81 + drawOverlayCanvas(); 82 + 83 + // Simulation config 84 + const config = { 85 + SIM_RESOLUTION: 128, 86 + DYE_RESOLUTION: 1024, 87 + CAPTURE_RESOLUTION: 512, 88 + DENSITY_DISSIPATION: 1.0, 89 + VELOCITY_DISSIPATION: 0.1, 90 + PRESSURE: 0.8, 91 + PRESSURE_ITERATIONS: 20, 92 + CURL: 30, 93 + SPLAT_RADIUS: 0.25, 94 + SPLAT_FORCE: 1000, 95 + SHADING: true, 96 + COLORFUL: true, 97 + COLOR_UPDATE_SPEED: 10, 98 + PAUSED: false, 99 + BACK_COLOR: { r: 0, g: 0, b: 0 }, 100 + TRANSPARENT: false, 101 + BLOOM: false, 102 + BLOOM_ITERATIONS: 8, 103 + BLOOM_RESOLUTION: 256, 104 + BLOOM_INTENSITY: 0.8, 105 + BLOOM_THRESHOLD: 0.8, 106 + BLOOM_SOFT_KNEE: 0.7, 107 + SUNRAYS: true, 108 + SUNRAYS_RESOLUTION: 196, 109 + SUNRAYS_WEIGHT: 1.0, 110 + START_HUE: 0.5, 111 + END_HUE: 1.0, 112 + RENDER_SPEED: 0.4 113 + }; 114 + 115 + function PointerPrototype() { 116 + return { 117 + id: -1, 118 + texcoordX: 0, 119 + texcoordY: 0, 120 + prevTexcoordX: 0, 121 + prevTexcoordY: 0, 122 + deltaX: 0, 123 + deltaY: 0, 124 + down: false, 125 + moved: false, 126 + color: [30, 0, 300] as [number, number, number] 127 + }; 128 + } 129 + 130 + type Pointer = ReturnType<typeof PointerPrototype>; 131 + let pointers: Pointer[] = [PointerPrototype()]; 132 + let splatStack: number[] = []; 133 + 134 + const { gl, ext } = getWebGLContext(fluidCanvas); 135 + if (!gl) return; 136 + 137 + if (isMobile()) { 138 + config.DYE_RESOLUTION = 512; 139 + } 140 + if (!ext.supportLinearFiltering) { 141 + config.DYE_RESOLUTION = 512; 142 + config.SHADING = false; 143 + config.BLOOM = false; 144 + config.SUNRAYS = false; 145 + } 146 + 147 + function getWebGLContext(canvas: HTMLCanvasElement) { 148 + const params = { 149 + alpha: true, 150 + depth: false, 151 + stencil: true, 152 + antialias: false, 153 + preserveDrawingBuffer: false 154 + }; 155 + 156 + let gl = canvas.getContext('webgl2', params) as WebGL2RenderingContext | null; 157 + const isWebGL2 = !!gl; 158 + if (!isWebGL2) { 159 + gl = (canvas.getContext('webgl', params) || 160 + canvas.getContext('experimental-webgl', params)) as WebGL2RenderingContext | null; 161 + } 162 + 163 + if (!gl) return { gl: null, ext: { supportLinearFiltering: false } as any }; 164 + 165 + let halfFloat: any; 166 + let supportLinearFiltering: any; 167 + if (isWebGL2) { 168 + gl.getExtension('EXT_color_buffer_float'); 169 + supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); 170 + } else { 171 + halfFloat = gl.getExtension('OES_texture_half_float'); 172 + supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); 173 + } 174 + 175 + gl.clearColor(0.0, 0.0, 0.0, 1.0); 176 + 177 + const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat?.HALF_FLOAT_OES; 178 + let formatRGBA: any; 179 + let formatRG: any; 180 + let formatR: any; 181 + 182 + if (isWebGL2) { 183 + formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); 184 + formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); 185 + formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); 186 + } else { 187 + formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 188 + formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 189 + formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 190 + } 191 + 192 + return { 193 + gl, 194 + ext: { 195 + formatRGBA, 196 + formatRG, 197 + formatR, 198 + halfFloatTexType, 199 + supportLinearFiltering 200 + } 201 + }; 202 + } 203 + 204 + function getSupportedFormat( 205 + gl: WebGL2RenderingContext, 206 + internalFormat: number, 207 + format: number, 208 + type: number 209 + ): { internalFormat: number; format: number } | null { 210 + if (!supportRenderTextureFormat(gl, internalFormat, format, type)) { 211 + switch (internalFormat) { 212 + case gl.R16F: 213 + return getSupportedFormat(gl, gl.RG16F, gl.RG, type); 214 + case gl.RG16F: 215 + return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); 216 + default: 217 + return null; 218 + } 219 + } 220 + return { internalFormat, format }; 221 + } 222 + 223 + function supportRenderTextureFormat( 224 + gl: WebGL2RenderingContext, 225 + internalFormat: number, 226 + format: number, 227 + type: number 228 + ) { 229 + const texture = gl.createTexture(); 230 + gl.bindTexture(gl.TEXTURE_2D, texture); 231 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 232 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 233 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 234 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 235 + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); 236 + 237 + const fbo = gl.createFramebuffer(); 238 + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 239 + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 240 + 241 + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); 242 + return status === gl.FRAMEBUFFER_COMPLETE; 243 + } 244 + 245 + function isMobile() { 246 + return /Mobi|Android/i.test(navigator.userAgent); 247 + } 248 + 249 + class Material { 250 + vertexShader: WebGLShader; 251 + fragmentShaderSource: string; 252 + programs: Record<number, WebGLProgram> = {}; 253 + activeProgram: WebGLProgram | null = null; 254 + uniforms: Record<string, WebGLUniformLocation | null> = {}; 255 + 256 + constructor(vertexShader: WebGLShader, fragmentShaderSource: string) { 257 + this.vertexShader = vertexShader; 258 + this.fragmentShaderSource = fragmentShaderSource; 259 + } 260 + 261 + setKeywords(keywords: string[]) { 262 + let hash = 0; 263 + for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]); 264 + 265 + let program = this.programs[hash]; 266 + if (!program) { 267 + const fragmentShader = compileShader( 268 + gl.FRAGMENT_SHADER, 269 + this.fragmentShaderSource, 270 + keywords 271 + ); 272 + program = createProgram(this.vertexShader, fragmentShader); 273 + this.programs[hash] = program; 274 + } 275 + 276 + if (program === this.activeProgram) return; 277 + 278 + this.uniforms = getUniforms(program); 279 + this.activeProgram = program; 280 + } 281 + 282 + bind() { 283 + gl.useProgram(this.activeProgram); 284 + } 285 + } 286 + 287 + class Program { 288 + uniforms: Record<string, WebGLUniformLocation | null> = {}; 289 + program: WebGLProgram; 290 + 291 + constructor(vertexShader: WebGLShader, fragmentShader: WebGLShader) { 292 + this.program = createProgram(vertexShader, fragmentShader); 293 + this.uniforms = getUniforms(this.program); 294 + } 295 + 296 + bind() { 297 + gl.useProgram(this.program); 298 + } 299 + } 300 + 301 + function createProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader) { 302 + const program = gl.createProgram()!; 303 + gl.attachShader(program, vertexShader); 304 + gl.attachShader(program, fragmentShader); 305 + gl.linkProgram(program); 306 + 307 + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 308 + console.trace(gl.getProgramInfoLog(program)); 309 + } 310 + 311 + return program; 312 + } 313 + 314 + function getUniforms(program: WebGLProgram) { 315 + const uniforms: Record<string, WebGLUniformLocation | null> = {}; 316 + const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 317 + for (let i = 0; i < uniformCount; i++) { 318 + const uniformName = gl.getActiveUniform(program, i)!.name; 319 + uniforms[uniformName] = gl.getUniformLocation(program, uniformName); 320 + } 321 + return uniforms; 322 + } 323 + 324 + function compileShader(type: number, source: string, keywords?: string[]) { 325 + source = addKeywords(source, keywords); 326 + 327 + const shader = gl.createShader(type)!; 328 + gl.shaderSource(shader, source); 329 + gl.compileShader(shader); 330 + 331 + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 332 + console.trace(gl.getShaderInfoLog(shader)); 333 + } 334 + 335 + return shader; 336 + } 337 + 338 + function addKeywords(source: string, keywords?: string[]) { 339 + if (!keywords) return source; 340 + let keywordsString = ''; 341 + keywords.forEach((keyword) => { 342 + keywordsString += '#define ' + keyword + '\n'; 343 + }); 344 + return keywordsString + source; 345 + } 346 + 347 + const baseVertexShader = compileShader( 348 + gl.VERTEX_SHADER, 349 + ` 350 + precision highp float; 351 + attribute vec2 aPosition; 352 + varying vec2 vUv; 353 + varying vec2 vL; 354 + varying vec2 vR; 355 + varying vec2 vT; 356 + varying vec2 vB; 357 + uniform vec2 texelSize; 358 + void main () { 359 + vUv = aPosition * 0.5 + 0.5; 360 + vL = vUv - vec2(texelSize.x, 0.0); 361 + vR = vUv + vec2(texelSize.x, 0.0); 362 + vT = vUv + vec2(0.0, texelSize.y); 363 + vB = vUv - vec2(0.0, texelSize.y); 364 + gl_Position = vec4(aPosition, 0.0, 1.0); 365 + } 366 + ` 367 + ); 368 + 369 + const blurVertexShader = compileShader( 370 + gl.VERTEX_SHADER, 371 + ` 372 + precision highp float; 373 + attribute vec2 aPosition; 374 + varying vec2 vUv; 375 + varying vec2 vL; 376 + varying vec2 vR; 377 + uniform vec2 texelSize; 378 + void main () { 379 + vUv = aPosition * 0.5 + 0.5; 380 + float offset = 1.33333333; 381 + vL = vUv - texelSize * offset; 382 + vR = vUv + texelSize * offset; 383 + gl_Position = vec4(aPosition, 0.0, 1.0); 384 + } 385 + ` 386 + ); 387 + 388 + const blurShader = compileShader( 389 + gl.FRAGMENT_SHADER, 390 + ` 391 + precision mediump float; 392 + precision mediump sampler2D; 393 + varying vec2 vUv; 394 + varying vec2 vL; 395 + varying vec2 vR; 396 + uniform sampler2D uTexture; 397 + void main () { 398 + vec4 sum = texture2D(uTexture, vUv) * 0.29411764; 399 + sum += texture2D(uTexture, vL) * 0.35294117; 400 + sum += texture2D(uTexture, vR) * 0.35294117; 401 + gl_FragColor = sum; 402 + } 403 + ` 404 + ); 405 + 406 + const copyShader = compileShader( 407 + gl.FRAGMENT_SHADER, 408 + ` 409 + precision mediump float; 410 + precision mediump sampler2D; 411 + varying highp vec2 vUv; 412 + uniform sampler2D uTexture; 413 + void main () { 414 + gl_FragColor = texture2D(uTexture, vUv); 415 + } 416 + ` 417 + ); 418 + 419 + const clearShader = compileShader( 420 + gl.FRAGMENT_SHADER, 421 + ` 422 + precision mediump float; 423 + precision mediump sampler2D; 424 + varying highp vec2 vUv; 425 + uniform sampler2D uTexture; 426 + uniform float value; 427 + void main () { 428 + gl_FragColor = value * texture2D(uTexture, vUv); 429 + } 430 + ` 431 + ); 432 + 433 + const colorShader = compileShader( 434 + gl.FRAGMENT_SHADER, 435 + ` 436 + precision mediump float; 437 + uniform vec4 color; 438 + void main () { 439 + gl_FragColor = color; 440 + } 441 + ` 442 + ); 443 + 444 + const displayShaderSource = ` 445 + precision highp float; 446 + precision highp sampler2D; 447 + varying vec2 vUv; 448 + varying vec2 vL; 449 + varying vec2 vR; 450 + varying vec2 vT; 451 + varying vec2 vB; 452 + uniform sampler2D uTexture; 453 + uniform sampler2D uBloom; 454 + uniform sampler2D uSunrays; 455 + uniform sampler2D uDithering; 456 + uniform vec2 ditherScale; 457 + uniform vec2 texelSize; 458 + vec3 linearToGamma (vec3 color) { 459 + color = max(color, vec3(0)); 460 + return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); 461 + } 462 + void main () { 463 + vec3 c = texture2D(uTexture, vUv).rgb; 464 + #ifdef SHADING 465 + vec3 lc = texture2D(uTexture, vL).rgb; 466 + vec3 rc = texture2D(uTexture, vR).rgb; 467 + vec3 tc = texture2D(uTexture, vT).rgb; 468 + vec3 bc = texture2D(uTexture, vB).rgb; 469 + float dx = length(rc) - length(lc); 470 + float dy = length(tc) - length(bc); 471 + vec3 n = normalize(vec3(dx, dy, length(texelSize))); 472 + vec3 l = vec3(0.0, 0.0, 1.0); 473 + float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); 474 + c *= diffuse; 475 + #endif 476 + #ifdef BLOOM 477 + vec3 bloom = texture2D(uBloom, vUv).rgb; 478 + #endif 479 + #ifdef SUNRAYS 480 + float sunrays = texture2D(uSunrays, vUv).r; 481 + c *= sunrays; 482 + #ifdef BLOOM 483 + bloom *= sunrays; 484 + #endif 485 + #endif 486 + #ifdef BLOOM 487 + float noise = texture2D(uDithering, vUv * ditherScale).r; 488 + noise = noise * 2.0 - 1.0; 489 + bloom += noise / 255.0; 490 + bloom = linearToGamma(bloom); 491 + c += bloom; 492 + #endif 493 + float a = max(c.r, max(c.g, c.b)); 494 + gl_FragColor = vec4(c, a); 495 + } 496 + `; 497 + 498 + const splatShader = compileShader( 499 + gl.FRAGMENT_SHADER, 500 + ` 501 + precision highp float; 502 + precision highp sampler2D; 503 + varying vec2 vUv; 504 + uniform sampler2D uTarget; 505 + uniform float aspectRatio; 506 + uniform vec3 color; 507 + uniform vec2 point; 508 + uniform float radius; 509 + void main () { 510 + vec2 p = vUv - point.xy; 511 + p.x *= aspectRatio; 512 + vec3 splat = exp(-dot(p, p) / radius) * color; 513 + vec3 base = texture2D(uTarget, vUv).xyz; 514 + gl_FragColor = vec4(base + splat, 1.0); 515 + } 516 + ` 517 + ); 518 + 519 + const advectionShader = compileShader( 520 + gl.FRAGMENT_SHADER, 521 + ` 522 + precision highp float; 523 + precision highp sampler2D; 524 + varying vec2 vUv; 525 + uniform sampler2D uVelocity; 526 + uniform sampler2D uSource; 527 + uniform vec2 texelSize; 528 + uniform vec2 dyeTexelSize; 529 + uniform float dt; 530 + uniform float dissipation; 531 + vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { 532 + vec2 st = uv / tsize - 0.5; 533 + vec2 iuv = floor(st); 534 + vec2 fuv = fract(st); 535 + vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); 536 + vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); 537 + vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); 538 + vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); 539 + return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); 540 + } 541 + void main () { 542 + #ifdef MANUAL_FILTERING 543 + vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; 544 + vec4 result = bilerp(uSource, coord, dyeTexelSize); 545 + #else 546 + vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; 547 + vec4 result = texture2D(uSource, coord); 548 + #endif 549 + float decay = 1.0 + dissipation * dt; 550 + gl_FragColor = result / decay; 551 + }`, 552 + ext.supportLinearFiltering ? undefined : ['MANUAL_FILTERING'] 553 + ); 554 + 555 + const divergenceShader = compileShader( 556 + gl.FRAGMENT_SHADER, 557 + ` 558 + precision mediump float; 559 + precision mediump sampler2D; 560 + varying highp vec2 vUv; 561 + varying highp vec2 vL; 562 + varying highp vec2 vR; 563 + varying highp vec2 vT; 564 + varying highp vec2 vB; 565 + uniform sampler2D uVelocity; 566 + void main () { 567 + float L = texture2D(uVelocity, vL).x; 568 + float R = texture2D(uVelocity, vR).x; 569 + float T = texture2D(uVelocity, vT).y; 570 + float B = texture2D(uVelocity, vB).y; 571 + vec2 C = texture2D(uVelocity, vUv).xy; 572 + if (vL.x < 0.0) { L = -C.x; } 573 + if (vR.x > 1.0) { R = -C.x; } 574 + if (vT.y > 1.0) { T = -C.y; } 575 + if (vB.y < 0.0) { B = -C.y; } 576 + float div = 0.5 * (R - L + T - B); 577 + gl_FragColor = vec4(div, 0.0, 0.0, 1.0); 578 + } 579 + ` 580 + ); 581 + 582 + const curlShader = compileShader( 583 + gl.FRAGMENT_SHADER, 584 + ` 585 + precision mediump float; 586 + precision mediump sampler2D; 587 + varying highp vec2 vUv; 588 + varying highp vec2 vL; 589 + varying highp vec2 vR; 590 + varying highp vec2 vT; 591 + varying highp vec2 vB; 592 + uniform sampler2D uVelocity; 593 + void main () { 594 + float L = texture2D(uVelocity, vL).y; 595 + float R = texture2D(uVelocity, vR).y; 596 + float T = texture2D(uVelocity, vT).x; 597 + float B = texture2D(uVelocity, vB).x; 598 + float vorticity = R - L - T + B; 599 + gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); 600 + } 601 + ` 602 + ); 603 + 604 + const vorticityShader = compileShader( 605 + gl.FRAGMENT_SHADER, 606 + ` 607 + precision highp float; 608 + precision highp sampler2D; 609 + varying vec2 vUv; 610 + varying vec2 vL; 611 + varying vec2 vR; 612 + varying vec2 vT; 613 + varying vec2 vB; 614 + uniform sampler2D uVelocity; 615 + uniform sampler2D uCurl; 616 + uniform float curl; 617 + uniform float dt; 618 + void main () { 619 + float L = texture2D(uCurl, vL).x; 620 + float R = texture2D(uCurl, vR).x; 621 + float T = texture2D(uCurl, vT).x; 622 + float B = texture2D(uCurl, vB).x; 623 + float C = texture2D(uCurl, vUv).x; 624 + vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); 625 + force /= length(force) + 0.0001; 626 + force *= curl * C; 627 + force.y *= -1.0; 628 + vec2 velocity = texture2D(uVelocity, vUv).xy; 629 + velocity += force * dt; 630 + velocity = min(max(velocity, -1000.0), 1000.0); 631 + gl_FragColor = vec4(velocity, 0.0, 1.0); 632 + } 633 + ` 634 + ); 635 + 636 + const pressureShader = compileShader( 637 + gl.FRAGMENT_SHADER, 638 + ` 639 + precision mediump float; 640 + precision mediump sampler2D; 641 + varying highp vec2 vUv; 642 + varying highp vec2 vL; 643 + varying highp vec2 vR; 644 + varying highp vec2 vT; 645 + varying highp vec2 vB; 646 + uniform sampler2D uPressure; 647 + uniform sampler2D uDivergence; 648 + void main () { 649 + float L = texture2D(uPressure, vL).x; 650 + float R = texture2D(uPressure, vR).x; 651 + float T = texture2D(uPressure, vT).x; 652 + float B = texture2D(uPressure, vB).x; 653 + float C = texture2D(uPressure, vUv).x; 654 + float divergence = texture2D(uDivergence, vUv).x; 655 + float pressure = (L + R + B + T - divergence) * 0.25; 656 + gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); 657 + } 658 + ` 659 + ); 660 + 661 + const gradientSubtractShader = compileShader( 662 + gl.FRAGMENT_SHADER, 663 + ` 664 + precision mediump float; 665 + precision mediump sampler2D; 666 + varying highp vec2 vUv; 667 + varying highp vec2 vL; 668 + varying highp vec2 vR; 669 + varying highp vec2 vT; 670 + varying highp vec2 vB; 671 + uniform sampler2D uPressure; 672 + uniform sampler2D uVelocity; 673 + void main () { 674 + float L = texture2D(uPressure, vL).x; 675 + float R = texture2D(uPressure, vR).x; 676 + float T = texture2D(uPressure, vT).x; 677 + float B = texture2D(uPressure, vB).x; 678 + vec2 velocity = texture2D(uVelocity, vUv).xy; 679 + velocity.xy -= vec2(R - L, T - B); 680 + gl_FragColor = vec4(velocity, 0.0, 1.0); 681 + } 682 + ` 683 + ); 684 + 685 + const sunraysMaskShader = compileShader( 686 + gl.FRAGMENT_SHADER, 687 + ` 688 + precision highp float; 689 + precision highp sampler2D; 690 + varying vec2 vUv; 691 + uniform sampler2D uTexture; 692 + void main () { 693 + vec4 c = texture2D(uTexture, vUv); 694 + float br = max(c.r, max(c.g, c.b)); 695 + c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8); 696 + gl_FragColor = c; 697 + } 698 + ` 699 + ); 700 + 701 + const sunraysShader = compileShader( 702 + gl.FRAGMENT_SHADER, 703 + ` 704 + precision highp float; 705 + precision highp sampler2D; 706 + varying vec2 vUv; 707 + uniform sampler2D uTexture; 708 + uniform float weight; 709 + #define ITERATIONS 16 710 + void main () { 711 + float Density = 0.3; 712 + float Decay = 0.95; 713 + float Exposure = 0.7; 714 + vec2 coord = vUv; 715 + vec2 dir = vUv - 0.5; 716 + dir *= 1.0 / float(ITERATIONS) * Density; 717 + float illuminationDecay = 1.0; 718 + float color = texture2D(uTexture, vUv).a; 719 + for (int i = 0; i < ITERATIONS; i++) { 720 + coord -= dir; 721 + float col = texture2D(uTexture, coord).a; 722 + color += col * illuminationDecay * weight; 723 + illuminationDecay *= Decay; 724 + } 725 + gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0); 726 + } 727 + ` 728 + ); 729 + 730 + // Setup blit 731 + gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); 732 + gl.bufferData( 733 + gl.ARRAY_BUFFER, 734 + new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), 735 + gl.STATIC_DRAW 736 + ); 737 + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); 738 + gl.bufferData( 739 + gl.ELEMENT_ARRAY_BUFFER, 740 + new Uint16Array([0, 1, 2, 0, 2, 3]), 741 + gl.STATIC_DRAW 742 + ); 743 + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); 744 + gl.enableVertexAttribArray(0); 745 + 746 + type FBO = { 747 + texture: WebGLTexture; 748 + fbo: WebGLFramebuffer; 749 + width: number; 750 + height: number; 751 + texelSizeX: number; 752 + texelSizeY: number; 753 + attach: (id: number) => number; 754 + }; 755 + 756 + type DoubleFBO = { 757 + width: number; 758 + height: number; 759 + texelSizeX: number; 760 + texelSizeY: number; 761 + read: FBO; 762 + write: FBO; 763 + swap: () => void; 764 + }; 765 + 766 + function blit(target: FBO | null, clear = false) { 767 + if (target === null) { 768 + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 769 + gl.bindFramebuffer(gl.FRAMEBUFFER, null); 770 + } else { 771 + gl.viewport(0, 0, target.width, target.height); 772 + gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); 773 + } 774 + if (clear) { 775 + gl.clearColor(0.0, 0.0, 0.0, 1.0); 776 + gl.clear(gl.COLOR_BUFFER_BIT); 777 + } 778 + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); 779 + } 780 + 781 + let dye: DoubleFBO; 782 + let velocity: DoubleFBO; 783 + let divergence: FBO; 784 + let curl: FBO; 785 + let pressure: DoubleFBO; 786 + let sunrays: FBO; 787 + let sunraysTemp: FBO; 788 + 789 + const ditheringTexture = createTextureAsync(ditheringTextureUrl); 790 + 791 + const blurProgram = new Program(blurVertexShader, blurShader); 792 + const copyProgram = new Program(baseVertexShader, copyShader); 793 + const clearProgram = new Program(baseVertexShader, clearShader); 794 + const colorProgram = new Program(baseVertexShader, colorShader); 795 + const splatProgram = new Program(baseVertexShader, splatShader); 796 + const advectionProgram = new Program(baseVertexShader, advectionShader); 797 + const divergenceProgram = new Program(baseVertexShader, divergenceShader); 798 + const curlProgram = new Program(baseVertexShader, curlShader); 799 + const vorticityProgram = new Program(baseVertexShader, vorticityShader); 800 + const pressureProgram = new Program(baseVertexShader, pressureShader); 801 + const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); 802 + const sunraysMaskProgram = new Program(baseVertexShader, sunraysMaskShader); 803 + const sunraysProgram = new Program(baseVertexShader, sunraysShader); 804 + 805 + const displayMaterial = new Material(baseVertexShader, displayShaderSource); 806 + 807 + function getResolution(resolution: number) { 808 + let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; 809 + if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio; 810 + const min = Math.round(resolution); 811 + const max = Math.round(resolution * aspectRatio); 812 + if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min }; 813 + else return { width: min, height: max }; 814 + } 815 + 816 + function createFBO( 817 + w: number, 818 + h: number, 819 + internalFormat: number, 820 + format: number, 821 + type: number, 822 + param: number 823 + ): FBO { 824 + gl.activeTexture(gl.TEXTURE0); 825 + const texture = gl.createTexture()!; 826 + gl.bindTexture(gl.TEXTURE_2D, texture); 827 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); 828 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); 829 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 830 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 831 + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); 832 + 833 + const fbo = gl.createFramebuffer()!; 834 + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 835 + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 836 + gl.viewport(0, 0, w, h); 837 + gl.clear(gl.COLOR_BUFFER_BIT); 838 + 839 + const texelSizeX = 1.0 / w; 840 + const texelSizeY = 1.0 / h; 841 + 842 + return { 843 + texture, 844 + fbo, 845 + width: w, 846 + height: h, 847 + texelSizeX, 848 + texelSizeY, 849 + attach(id: number) { 850 + gl.activeTexture(gl.TEXTURE0 + id); 851 + gl.bindTexture(gl.TEXTURE_2D, texture); 852 + return id; 853 + } 854 + }; 855 + } 856 + 857 + function createDoubleFBO( 858 + w: number, 859 + h: number, 860 + internalFormat: number, 861 + format: number, 862 + type: number, 863 + param: number 864 + ): DoubleFBO { 865 + let fbo1 = createFBO(w, h, internalFormat, format, type, param); 866 + let fbo2 = createFBO(w, h, internalFormat, format, type, param); 867 + 868 + return { 869 + width: w, 870 + height: h, 871 + texelSizeX: fbo1.texelSizeX, 872 + texelSizeY: fbo1.texelSizeY, 873 + get read() { 874 + return fbo1; 875 + }, 876 + set read(value) { 877 + fbo1 = value; 878 + }, 879 + get write() { 880 + return fbo2; 881 + }, 882 + set write(value) { 883 + fbo2 = value; 884 + }, 885 + swap() { 886 + const temp = fbo1; 887 + fbo1 = fbo2; 888 + fbo2 = temp; 889 + } 890 + }; 891 + } 892 + 893 + function resizeFBO( 894 + target: FBO, 895 + w: number, 896 + h: number, 897 + internalFormat: number, 898 + format: number, 899 + type: number, 900 + param: number 901 + ) { 902 + const newFBO = createFBO(w, h, internalFormat, format, type, param); 903 + copyProgram.bind(); 904 + gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); 905 + blit(newFBO); 906 + return newFBO; 907 + } 908 + 909 + function resizeDoubleFBO( 910 + target: DoubleFBO, 911 + w: number, 912 + h: number, 913 + internalFormat: number, 914 + format: number, 915 + type: number, 916 + param: number 917 + ) { 918 + if (target.width === w && target.height === h) return target; 919 + target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); 920 + target.write = createFBO(w, h, internalFormat, format, type, param); 921 + target.width = w; 922 + target.height = h; 923 + target.texelSizeX = 1.0 / w; 924 + target.texelSizeY = 1.0 / h; 925 + return target; 926 + } 927 + 928 + function createTextureAsync(url: string) { 929 + const texture = gl.createTexture()!; 930 + gl.bindTexture(gl.TEXTURE_2D, texture); 931 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 932 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 933 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); 934 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); 935 + gl.texImage2D( 936 + gl.TEXTURE_2D, 937 + 0, 938 + gl.RGB, 939 + 1, 940 + 1, 941 + 0, 942 + gl.RGB, 943 + gl.UNSIGNED_BYTE, 944 + new Uint8Array([255, 255, 255]) 945 + ); 946 + 947 + const obj = { 948 + texture, 949 + width: 1, 950 + height: 1, 951 + attach(id: number) { 952 + gl.activeTexture(gl.TEXTURE0 + id); 953 + gl.bindTexture(gl.TEXTURE_2D, texture); 954 + return id; 955 + } 956 + }; 957 + 958 + const image = new Image(); 959 + image.onload = () => { 960 + obj.width = image.width; 961 + obj.height = image.height; 962 + gl.bindTexture(gl.TEXTURE_2D, texture); 963 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 964 + }; 965 + image.src = url; 966 + 967 + return obj; 968 + } 969 + 970 + function initFramebuffers() { 971 + const simRes = getResolution(config.SIM_RESOLUTION); 972 + const dyeRes = getResolution(config.DYE_RESOLUTION); 973 + 974 + const texType = ext.halfFloatTexType; 975 + const rgba = ext.formatRGBA; 976 + const rg = ext.formatRG; 977 + const r = ext.formatR; 978 + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; 979 + 980 + gl.disable(gl.BLEND); 981 + 982 + if (!dye) { 983 + dye = createDoubleFBO( 984 + dyeRes.width, 985 + dyeRes.height, 986 + rgba.internalFormat, 987 + rgba.format, 988 + texType, 989 + filtering 990 + ); 991 + } else { 992 + dye = resizeDoubleFBO( 993 + dye, 994 + dyeRes.width, 995 + dyeRes.height, 996 + rgba.internalFormat, 997 + rgba.format, 998 + texType, 999 + filtering 1000 + ); 1001 + } 1002 + 1003 + if (!velocity) { 1004 + velocity = createDoubleFBO( 1005 + simRes.width, 1006 + simRes.height, 1007 + rg.internalFormat, 1008 + rg.format, 1009 + texType, 1010 + filtering 1011 + ); 1012 + } else { 1013 + velocity = resizeDoubleFBO( 1014 + velocity, 1015 + simRes.width, 1016 + simRes.height, 1017 + rg.internalFormat, 1018 + rg.format, 1019 + texType, 1020 + filtering 1021 + ); 1022 + } 1023 + 1024 + divergence = createFBO( 1025 + simRes.width, 1026 + simRes.height, 1027 + r.internalFormat, 1028 + r.format, 1029 + texType, 1030 + gl.NEAREST 1031 + ); 1032 + curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); 1033 + pressure = createDoubleFBO( 1034 + simRes.width, 1035 + simRes.height, 1036 + r.internalFormat, 1037 + r.format, 1038 + texType, 1039 + gl.NEAREST 1040 + ); 1041 + 1042 + initSunraysFramebuffers(); 1043 + } 1044 + 1045 + function initSunraysFramebuffers() { 1046 + const res = getResolution(config.SUNRAYS_RESOLUTION); 1047 + const texType = ext.halfFloatTexType; 1048 + const r = ext.formatR; 1049 + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; 1050 + 1051 + sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); 1052 + sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); 1053 + } 1054 + 1055 + function updateKeywords() { 1056 + const displayKeywords: string[] = []; 1057 + if (config.SHADING) displayKeywords.push('SHADING'); 1058 + if (config.SUNRAYS) displayKeywords.push('SUNRAYS'); 1059 + displayMaterial.setKeywords(displayKeywords); 1060 + } 1061 + 1062 + function scaleByPixelRatio(input: number) { 1063 + const pixelRatio = window.devicePixelRatio || 1; 1064 + return Math.floor(input * pixelRatio); 1065 + } 1066 + 1067 + function resizeCanvas() { 1068 + const width = scaleByPixelRatio(fluidCanvas.clientWidth); 1069 + const height = scaleByPixelRatio(fluidCanvas.clientHeight); 1070 + if (fluidCanvas.width !== width || fluidCanvas.height !== height) { 1071 + fluidCanvas.width = width; 1072 + fluidCanvas.height = height; 1073 + drawOverlayCanvas(); 1074 + return true; 1075 + } 1076 + return false; 1077 + } 1078 + 1079 + function HSVtoRGB(h: number, s: number, v: number) { 1080 + let r = 0, 1081 + g = 0, 1082 + b = 0; 1083 + const i = Math.floor(h * 6); 1084 + const f = h * 6 - i; 1085 + const p = v * (1 - s); 1086 + const q = v * (1 - f * s); 1087 + const t = v * (1 - (1 - f) * s); 1088 + 1089 + switch (i % 6) { 1090 + case 0: 1091 + (r = v), (g = t), (b = p); 1092 + break; 1093 + case 1: 1094 + (r = q), (g = v), (b = p); 1095 + break; 1096 + case 2: 1097 + (r = p), (g = v), (b = t); 1098 + break; 1099 + case 3: 1100 + (r = p), (g = q), (b = v); 1101 + break; 1102 + case 4: 1103 + (r = t), (g = p), (b = v); 1104 + break; 1105 + case 5: 1106 + (r = v), (g = p), (b = q); 1107 + break; 1108 + } 1109 + 1110 + return { r, g, b }; 1111 + } 1112 + 1113 + function generateColor() { 1114 + const c = HSVtoRGB( 1115 + Math.random() * (config.END_HUE - config.START_HUE) + config.START_HUE, 1116 + 1.0, 1117 + 1.0 1118 + ); 1119 + c.r *= 0.15; 1120 + c.g *= 0.15; 1121 + c.b *= 0.15; 1122 + return c; 1123 + } 1124 + 1125 + function correctRadius(radius: number) { 1126 + const aspectRatio = fluidCanvas.width / fluidCanvas.height; 1127 + if (aspectRatio > 1) radius *= aspectRatio; 1128 + return radius; 1129 + } 1130 + 1131 + function splat(x: number, y: number, dx: number, dy: number, color: { r: number; g: number; b: number }) { 1132 + splatProgram.bind(); 1133 + gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); 1134 + gl.uniform1f(splatProgram.uniforms.aspectRatio, fluidCanvas.width / fluidCanvas.height); 1135 + gl.uniform2f(splatProgram.uniforms.point, x, y); 1136 + gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); 1137 + gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); 1138 + blit(velocity.write); 1139 + velocity.swap(); 1140 + 1141 + gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); 1142 + gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); 1143 + blit(dye.write); 1144 + dye.swap(); 1145 + } 1146 + 1147 + function multipleSplats(amount: number) { 1148 + for (let i = 0; i < amount; i++) { 1149 + const color = generateColor(); 1150 + color.r *= 10.0; 1151 + color.g *= 10.0; 1152 + color.b *= 10.0; 1153 + const x = Math.random(); 1154 + const y = Math.random() < 0.5 ? 0.8 : 0.2; 1155 + const dx = 100 * (Math.random() - 0.5); 1156 + const dy = 1000 * (Math.random() - 0.5); 1157 + splat(x, y, dx, dy, color); 1158 + } 1159 + } 1160 + 1161 + function splatPointer(pointer: Pointer) { 1162 + const dx = pointer.deltaX * config.SPLAT_FORCE * 5; 1163 + const dy = pointer.deltaY * config.SPLAT_FORCE * 5; 1164 + splat(pointer.texcoordX, pointer.texcoordY, dx, dy, { 1165 + r: pointer.color[0], 1166 + g: pointer.color[1], 1167 + b: pointer.color[2] 1168 + }); 1169 + } 1170 + 1171 + function step(dt: number) { 1172 + gl.disable(gl.BLEND); 1173 + 1174 + curlProgram.bind(); 1175 + gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1176 + gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); 1177 + blit(curl); 1178 + 1179 + vorticityProgram.bind(); 1180 + gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1181 + gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); 1182 + gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); 1183 + gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); 1184 + gl.uniform1f(vorticityProgram.uniforms.dt, dt); 1185 + blit(velocity.write); 1186 + velocity.swap(); 1187 + 1188 + divergenceProgram.bind(); 1189 + gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1190 + gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); 1191 + blit(divergence); 1192 + 1193 + clearProgram.bind(); 1194 + gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); 1195 + gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); 1196 + blit(pressure.write); 1197 + pressure.swap(); 1198 + 1199 + pressureProgram.bind(); 1200 + gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1201 + gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); 1202 + for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { 1203 + gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); 1204 + blit(pressure.write); 1205 + pressure.swap(); 1206 + } 1207 + 1208 + gradienSubtractProgram.bind(); 1209 + gl.uniform2f( 1210 + gradienSubtractProgram.uniforms.texelSize, 1211 + velocity.texelSizeX, 1212 + velocity.texelSizeY 1213 + ); 1214 + gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); 1215 + gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); 1216 + blit(velocity.write); 1217 + velocity.swap(); 1218 + 1219 + advectionProgram.bind(); 1220 + gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1221 + if (!ext.supportLinearFiltering) { 1222 + gl.uniform2f( 1223 + advectionProgram.uniforms.dyeTexelSize, 1224 + velocity.texelSizeX, 1225 + velocity.texelSizeY 1226 + ); 1227 + } 1228 + const velocityId = velocity.read.attach(0); 1229 + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); 1230 + gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); 1231 + gl.uniform1f(advectionProgram.uniforms.dt, dt); 1232 + gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); 1233 + blit(velocity.write); 1234 + velocity.swap(); 1235 + 1236 + if (!ext.supportLinearFiltering) { 1237 + gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); 1238 + } 1239 + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); 1240 + gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); 1241 + gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); 1242 + blit(dye.write); 1243 + dye.swap(); 1244 + } 1245 + 1246 + function blur(target: FBO, temp: FBO, iterations: number) { 1247 + blurProgram.bind(); 1248 + for (let i = 0; i < iterations; i++) { 1249 + gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0); 1250 + gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)); 1251 + blit(temp); 1252 + 1253 + gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY); 1254 + gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)); 1255 + blit(target); 1256 + } 1257 + } 1258 + 1259 + function applySunrays(source: FBO, mask: FBO, destination: FBO) { 1260 + gl.disable(gl.BLEND); 1261 + sunraysMaskProgram.bind(); 1262 + gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)); 1263 + blit(mask); 1264 + 1265 + sunraysProgram.bind(); 1266 + gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT); 1267 + gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)); 1268 + blit(destination); 1269 + } 1270 + 1271 + function drawColor(target: FBO | null, color: { r: number; g: number; b: number }) { 1272 + colorProgram.bind(); 1273 + gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1); 1274 + blit(target); 1275 + } 1276 + 1277 + function drawDisplay(target: FBO | null) { 1278 + const width = target === null ? gl.drawingBufferWidth : target.width; 1279 + const height = target === null ? gl.drawingBufferHeight : target.height; 1280 + 1281 + displayMaterial.bind(); 1282 + if (config.SHADING) { 1283 + gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); 1284 + } 1285 + gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); 1286 + if (config.SUNRAYS) { 1287 + gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)); 1288 + } 1289 + blit(target); 1290 + } 1291 + 1292 + function render(target: FBO | null) { 1293 + if (config.SUNRAYS) { 1294 + applySunrays(dye.read, dye.write, sunrays); 1295 + blur(sunrays, sunraysTemp, 1); 1296 + } 1297 + 1298 + if (target === null || !config.TRANSPARENT) { 1299 + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 1300 + gl.enable(gl.BLEND); 1301 + } else { 1302 + gl.disable(gl.BLEND); 1303 + } 1304 + 1305 + if (!config.TRANSPARENT) { 1306 + drawColor(target, { 1307 + r: config.BACK_COLOR.r / 255, 1308 + g: config.BACK_COLOR.g / 255, 1309 + b: config.BACK_COLOR.b / 255 1310 + }); 1311 + } 1312 + drawDisplay(target); 1313 + } 1314 + 1315 + function wrap(value: number, min: number, max: number) { 1316 + const range = max - min; 1317 + if (range === 0) return min; 1318 + return ((value - min) % range) + min; 1319 + } 1320 + 1321 + let lastUpdateTime = Date.now(); 1322 + let colorUpdateTimer = 0.0; 1323 + 1324 + function updateColors(dt: number) { 1325 + if (!config.COLORFUL) return; 1326 + colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; 1327 + if (colorUpdateTimer >= 1) { 1328 + colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); 1329 + pointers.forEach((p) => { 1330 + const c = generateColor(); 1331 + p.color = [c.r * 255, c.g * 255, c.b * 255]; 1332 + }); 1333 + } 1334 + } 1335 + 1336 + function applyInputs() { 1337 + if (splatStack.length > 0) multipleSplats(splatStack.pop()!); 1338 + 1339 + pointers.forEach((p) => { 1340 + if (p.moved) { 1341 + p.moved = false; 1342 + splatPointer(p); 1343 + } 1344 + }); 1345 + } 1346 + 1347 + function calcDeltaTime() { 1348 + const now = Date.now(); 1349 + let dt = (now - lastUpdateTime) / 1000; 1350 + dt = Math.min(dt, 0.016666); 1351 + lastUpdateTime = now; 1352 + return dt; 1353 + } 1354 + 1355 + function update() { 1356 + const dt = calcDeltaTime() * (config.RENDER_SPEED ?? 1.0); 1357 + if (resizeCanvas()) initFramebuffers(); 1358 + updateColors(dt); 1359 + applyInputs(); 1360 + if (!config.PAUSED) step(dt); 1361 + render(null); 1362 + animationId = requestAnimationFrame(update); 1363 + } 1364 + 1365 + function hashCode(s: string) { 1366 + if (s.length === 0) return 0; 1367 + let hash = 0; 1368 + for (let i = 0; i < s.length; i++) { 1369 + hash = (hash << 5) - hash + s.charCodeAt(i); 1370 + hash |= 0; 1371 + } 1372 + return hash; 1373 + } 1374 + 1375 + function correctDeltaX(delta: number) { 1376 + const aspectRatio = fluidCanvas.width / fluidCanvas.height; 1377 + if (aspectRatio < 1) delta *= aspectRatio; 1378 + return delta; 1379 + } 1380 + 1381 + function correctDeltaY(delta: number) { 1382 + const aspectRatio = fluidCanvas.width / fluidCanvas.height; 1383 + if (aspectRatio > 1) delta /= aspectRatio; 1384 + return delta; 1385 + } 1386 + 1387 + function updatePointerDownData(pointer: Pointer, id: number, posX: number, posY: number) { 1388 + pointer.id = id; 1389 + pointer.down = true; 1390 + pointer.moved = false; 1391 + pointer.texcoordX = posX / fluidCanvas.width; 1392 + pointer.texcoordY = 1.0 - posY / fluidCanvas.height; 1393 + pointer.prevTexcoordX = pointer.texcoordX; 1394 + pointer.prevTexcoordY = pointer.texcoordY; 1395 + pointer.deltaX = 0; 1396 + pointer.deltaY = 0; 1397 + const c = generateColor(); 1398 + pointer.color = [c.r * 255, c.g * 255, c.b * 255]; 1399 + } 1400 + 1401 + function updatePointerMoveData(pointer: Pointer, posX: number, posY: number) { 1402 + pointer.prevTexcoordX = pointer.texcoordX; 1403 + pointer.prevTexcoordY = pointer.texcoordY; 1404 + pointer.texcoordX = posX / fluidCanvas.width; 1405 + pointer.texcoordY = 1.0 - posY / fluidCanvas.height; 1406 + pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); 1407 + pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); 1408 + pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; 1409 + } 1410 + 1411 + function updatePointerUpData(pointer: Pointer) { 1412 + pointer.down = false; 1413 + } 1414 + 1415 + // Event handlers 1416 + fluidCanvas.addEventListener('mouseenter', (e) => { 1417 + // Create a small burst when mouse enters the card 1418 + const posX = scaleByPixelRatio(e.offsetX); 1419 + const posY = scaleByPixelRatio(e.offsetY); 1420 + const x = posX / fluidCanvas.width; 1421 + const y = 1.0 - posY / fluidCanvas.height; 1422 + const color = generateColor(); 1423 + color.r *= 10.0; 1424 + color.g *= 10.0; 1425 + color.b *= 10.0; 1426 + splat(x, y, 300 * (Math.random() - 0.5), 300 * (Math.random() - 0.5), color); 1427 + }); 1428 + 1429 + fluidCanvas.addEventListener('mousedown', (e) => { 1430 + const posX = scaleByPixelRatio(e.offsetX); 1431 + const posY = scaleByPixelRatio(e.offsetY); 1432 + let pointer = pointers.find((p) => p.id === -1); 1433 + if (!pointer) pointer = PointerPrototype(); 1434 + updatePointerDownData(pointer, -1, posX, posY); 1435 + }); 1436 + 1437 + fluidCanvas.addEventListener('mousemove', (e) => { 1438 + const pointer = pointers[0]; 1439 + const posX = scaleByPixelRatio(e.offsetX); 1440 + const posY = scaleByPixelRatio(e.offsetY); 1441 + updatePointerMoveData(pointer, posX, posY); 1442 + // Always create swish effect on hover 1443 + if (pointer.moved) { 1444 + pointer.moved = false; 1445 + // Generate a new color for visual interest 1446 + const c = generateColor(); 1447 + pointer.color = [c.r * 255, c.g * 255, c.b * 255]; 1448 + splatPointer(pointer); 1449 + } 1450 + }); 1451 + 1452 + fluidCanvas.addEventListener('mouseup', () => { 1453 + updatePointerUpData(pointers[0]); 1454 + }); 1455 + 1456 + fluidCanvas.addEventListener('touchstart', (e) => { 1457 + e.preventDefault(); 1458 + const touches = e.targetTouches; 1459 + while (touches.length >= pointers.length) pointers.push(PointerPrototype()); 1460 + for (let i = 0; i < touches.length; i++) { 1461 + const rect = fluidCanvas.getBoundingClientRect(); 1462 + const posX = scaleByPixelRatio(touches[i].clientX - rect.left); 1463 + const posY = scaleByPixelRatio(touches[i].clientY - rect.top); 1464 + updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); 1465 + } 1466 + }); 1467 + 1468 + fluidCanvas.addEventListener('touchmove', (e) => { 1469 + e.preventDefault(); 1470 + const touches = e.targetTouches; 1471 + for (let i = 0; i < touches.length; i++) { 1472 + const pointer = pointers[i + 1]; 1473 + if (!pointer.down) continue; 1474 + const rect = fluidCanvas.getBoundingClientRect(); 1475 + const posX = scaleByPixelRatio(touches[i].clientX - rect.left); 1476 + const posY = scaleByPixelRatio(touches[i].clientY - rect.top); 1477 + updatePointerMoveData(pointer, posX, posY); 1478 + } 1479 + }); 1480 + 1481 + fluidCanvas.addEventListener('touchend', (e) => { 1482 + const touches = e.changedTouches; 1483 + for (let i = 0; i < touches.length; i++) { 1484 + const pointer = pointers.find((p) => p.id === touches[i].identifier); 1485 + if (pointer) updatePointerUpData(pointer); 1486 + } 1487 + }); 1488 + 1489 + // Initialize 1490 + updateKeywords(); 1491 + initFramebuffers(); 1492 + multipleSplats(25); 1493 + update(); 1494 + 1495 + // Auto splat interval 1496 + splatIntervalId = setInterval(() => { 1497 + multipleSplats(5); 1498 + }, 500); 1499 + 1500 + // Resize observer - also triggers initial draw 1501 + resizeObserver = new ResizeObserver(() => { 1502 + resizeCanvas(); 1503 + drawOverlayCanvas(); 1504 + }); 1505 + resizeObserver.observe(container); 1506 + 1507 + // Mark as initialized after first resize callback 1508 + isInitialized = true; 1509 + } 1510 + </script> 1511 + 1512 + <div bind:this={container} class="relative h-full w-full overflow-hidden bg-black"> 1513 + <canvas bind:this={fluidCanvas} class="absolute h-full w-full"></canvas> 1514 + <canvas bind:this={maskCanvas} class="absolute h-full w-full"></canvas> 1515 + </div>
+127
src/lib/cards/FluidTextCard/FluidTextCardSettings.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import type { ContentComponentProps } from '../types'; 4 + import { ToggleGroup, ToggleGroupItem, Button, Input, Label } from '@foxui/core'; 5 + 6 + let { item = $bindable<Item>() }: ContentComponentProps = $props(); 7 + 8 + const fontWeights = ['400', '500', '600', '700', '800', '900'] as const; 9 + const fontFamilies = [ 10 + 'Arial', 11 + 'Helvetica', 12 + 'Georgia', 13 + 'Times New Roman', 14 + 'Courier New', 15 + 'monospace' 16 + ] as const; 17 + 18 + const classes = 'size-8 min-w-8 text-xs cursor-pointer'; 19 + </script> 20 + 21 + <div class="flex flex-col gap-3"> 22 + <div> 23 + <Label class="mb-1 text-xs">Text</Label> 24 + <Input 25 + bind:value={item.cardData.text} 26 + placeholder="Enter text" 27 + class="w-full" 28 + /> 29 + </div> 30 + 31 + <div> 32 + <Label class="mb-1 text-xs">Font Weight</Label> 33 + <ToggleGroup 34 + type="single" 35 + bind:value={ 36 + () => item.cardData.fontWeight ?? '900', 37 + (value) => { 38 + if (!value) return; 39 + item.cardData.fontWeight = value; 40 + } 41 + } 42 + > 43 + {#each fontWeights as weight (weight)} 44 + <ToggleGroupItem size="sm" value={weight} class={classes}> 45 + {weight} 46 + </ToggleGroupItem> 47 + {/each} 48 + </ToggleGroup> 49 + </div> 50 + 51 + <div> 52 + <Label class="mb-1 text-xs">Font Family</Label> 53 + <select 54 + class="w-full rounded-md border border-base-200 bg-base-50 px-2 py-1.5 text-sm dark:border-base-800 dark:bg-base-900" 55 + value={item.cardData.fontFamily ?? 'Arial'} 56 + onchange={(e) => { 57 + item.cardData.fontFamily = e.currentTarget.value; 58 + }} 59 + > 60 + {#each fontFamilies as font (font)} 61 + <option value={font}>{font}</option> 62 + {/each} 63 + </select> 64 + </div> 65 + 66 + <div> 67 + <Label class="mb-1 text-xs">Font Size ({Math.round((item.cardData.fontSize ?? 0.33) * 100)}%)</Label> 68 + <div class="flex items-center gap-2"> 69 + <Button 70 + variant="ghost" 71 + size="sm" 72 + onclick={() => { 73 + item.cardData.fontSize = Math.max((item.cardData.fontSize ?? 0.33) - 0.05, 0.1); 74 + }} 75 + disabled={(item.cardData.fontSize ?? 0.33) <= 0.1} 76 + > 77 + <svg 78 + xmlns="http://www.w3.org/2000/svg" 79 + width="16" 80 + height="16" 81 + viewBox="0 0 24 24" 82 + fill="none" 83 + stroke="currentColor" 84 + stroke-width="2" 85 + stroke-linecap="round" 86 + stroke-linejoin="round" 87 + > 88 + <path d="M5 12h14" /> 89 + </svg> 90 + </Button> 91 + <input 92 + type="range" 93 + min="0.1" 94 + max="0.8" 95 + step="0.01" 96 + value={item.cardData.fontSize ?? 0.33} 97 + oninput={(e) => { 98 + item.cardData.fontSize = parseFloat(e.currentTarget.value); 99 + }} 100 + class="h-2 w-full cursor-pointer appearance-none rounded-lg bg-base-200 dark:bg-base-700" 101 + /> 102 + <Button 103 + variant="ghost" 104 + size="sm" 105 + onclick={() => { 106 + item.cardData.fontSize = Math.min((item.cardData.fontSize ?? 0.33) + 0.05, 0.8); 107 + }} 108 + disabled={(item.cardData.fontSize ?? 0.33) >= 0.8} 109 + > 110 + <svg 111 + xmlns="http://www.w3.org/2000/svg" 112 + width="16" 113 + height="16" 114 + viewBox="0 0 24 24" 115 + fill="none" 116 + stroke="currentColor" 117 + stroke-width="2" 118 + stroke-linecap="round" 119 + stroke-linejoin="round" 120 + > 121 + <path d="M5 12h14" /> 122 + <path d="M12 5v14" /> 123 + </svg> 124 + </Button> 125 + </div> 126 + </div> 127 + </div>
+28
src/lib/cards/FluidTextCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import CreateFluidTextCardModal from './CreateFluidTextCardModal.svelte'; 3 + import FluidTextCard from './FluidTextCard.svelte'; 4 + import FluidTextCardSettings from './FluidTextCardSettings.svelte'; 5 + 6 + export const FluidTextCardDefinition = { 7 + type: 'fluid-text', 8 + contentComponent: FluidTextCard, 9 + createNew: (card) => { 10 + card.cardType = 'fluid-text'; 11 + card.cardData = { 12 + text: '', 13 + fontWeight: '900', 14 + fontFamily: 'Arial', 15 + fontSize: 0.33 16 + }; 17 + card.w = 4; 18 + card.h = 2; 19 + card.mobileW = 4; 20 + card.mobileH = 2; 21 + }, 22 + creationModalComponent: CreateFluidTextCardModal, 23 + settingsComponent: FluidTextCardSettings, 24 + sidebarButtonText: 'Fluid Text', 25 + defaultColor: 'transparent', 26 + minW: 2, 27 + minH: 2 28 + } as CardDefinition & { type: 'fluid-text' };
+2
src/lib/cards/FluidTextCard/text_effect_fluid-main/.gitignore
··· 1 + /.DS_Store 2 + /.vite
src/lib/cards/FluidTextCard/text_effect_fluid-main/LDR_LLL1_0.png

This is a binary file and will not be displayed.

+7
src/lib/cards/FluidTextCard/text_effect_fluid-main/LICENSE
··· 1 + Copyright 2024 flo-bit 2 + 3 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 + 5 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 + 7 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+71
src/lib/cards/FluidTextCard/text_effect_fluid-main/Readme.md
··· 1 + # Text effect: Fluid Simulation demo 2 + 3 + [Live demo](https://flo-bit.github.io/text_effect_fluid/) (currently sometimes breaks on first open, just reload once or twice) 4 + 5 + [![cover](cover.png)](https://flo-bit.github.io/text_effect_fluid/) 6 + 7 + Inspired by the current (Jan 2024) version of [the lumalabs homepage](https://lumalabs.ai/) (obviously, their effect looks *way* better, than my quick and dirty version): 8 + 9 + ![lumalabs](luma.png) 10 + 11 + Adjusted version of this awesome [WebGL Fluid Simulation](https://github.com/PavelDoGreat/WebGL-Fluid-Simulation). 12 + 13 + ## How it works 14 + 15 + I wrote a short [blog post](https://flobit.substack.com/p/how-i-recreated-this-awesome-text) about my approach. 16 + 17 + ## Usage 18 + 19 + - Add `script.js` and `LDR_LLL1_0.png` to your project 20 + 21 + - Add this to your html 22 + 23 + ```html 24 + <div id="canvasContainer"> 25 + <canvas></canvas> 26 + <canvas id="maskCanvas"> </canvas> 27 + </div> 28 + 29 + <script src="./script.js"></script> 30 + ``` 31 + 32 + - Add this styling 33 + 34 + ```css 35 + html, 36 + body { 37 + overflow: hidden; 38 + background-color: #000; 39 + } 40 + 41 + body { 42 + margin: 0; 43 + } 44 + 45 + #canvasContainer { 46 + position: relative; 47 + width: 100vw; 48 + height: 100vh; 49 + } 50 + 51 + canvas { 52 + width: 100%; 53 + height: 100%; 54 + position: absolute; 55 + } 56 + ``` 57 + 58 + 59 + ## License 60 + 61 + MIT License 62 + 63 + ``` 64 + Copyright 2024 flo-bit 65 + 66 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 67 + 68 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 69 + 70 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 71 + ```
src/lib/cards/FluidTextCard/text_effect_fluid-main/cover.png

This is a binary file and will not be displayed.

+46
src/lib/cards/FluidTextCard/text_effect_fluid-main/index.html
··· 1 + <!doctype html> 2 + <html lang="en" class="bg-black"> 3 + 4 + <head> 5 + <meta charset="utf-8" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + 8 + <title>fluid text effect</title> 9 + 10 + <style> 11 + html, 12 + body { 13 + overflow: hidden; 14 + background-color: #000; 15 + } 16 + 17 + body { 18 + margin: 0; 19 + } 20 + 21 + #canvasContainer { 22 + position: relative; 23 + width: 100vw; 24 + height: 100vh; 25 + } 26 + 27 + canvas { 28 + width: 100%; 29 + height: 100%; 30 + position: absolute; 31 + } 32 + </style> 33 + 34 + </head> 35 + 36 + <body> 37 + <!-- <input id="name"></input> --> 38 + <div id="canvasContainer"> 39 + <canvas></canvas> 40 + <canvas id="maskCanvas"> </canvas> 41 + </div> 42 + 43 + <script src="./script.js"></script> 44 + </body> 45 + 46 + </html>
src/lib/cards/FluidTextCard/text_effect_fluid-main/luma.png

This is a binary file and will not be displayed.

+1641
src/lib/cards/FluidTextCard/text_effect_fluid-main/script.js
··· 1 + /* 2 + MIT License 3 + 4 + Copyright (c) 2017 Pavel Dobryakov 5 + 6 + Permission is hereby granted, free of charge, to any person obtaining a copy 7 + of this software and associated documentation files (the "Software"), to deal 8 + in the Software without restriction, including without limitation the rights 9 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 + copies of the Software, and to permit persons to whom the Software is 11 + furnished to do so, subject to the following conditions: 12 + 13 + The above copyright notice and this permission notice shall be included in all 14 + copies or substantial portions of the Software. 15 + 16 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 + SOFTWARE. 23 + */ 24 + 25 + 'use strict'; 26 + 27 + const overlayConfig = { 28 + text: 'flo-bit', 29 + fontWidth: '900', 30 + font: 'Arial', 31 + fontSize: 1/3, // percentage to canvas width 32 + } 33 + 34 + // overlay canvas 35 + 36 + function resizeOverlayCanvas() { 37 + let overlayCanvas = document.getElementById('maskCanvas'); 38 + let container = document.getElementById('canvasContainer'); 39 + 40 + overlayCanvas.width = container.offsetWidth; 41 + overlayCanvas.height = container.offsetHeight; 42 + 43 + let ctx = overlayCanvas.getContext('2d'); 44 + 45 + let dpr = window.devicePixelRatio || 1; 46 + let rect = overlayCanvas.getBoundingClientRect(); 47 + 48 + overlayCanvas.width = rect.width * dpr; 49 + overlayCanvas.height = rect.height * dpr; 50 + ctx.scale(dpr, dpr); 51 + 52 + ctx.fillStyle = 'black'; 53 + ctx.fillRect(0, 0, overlayCanvas.width, overlayCanvas.height); 54 + 55 + let fontSize = Math.round(rect.width * overlayConfig.fontSize); 56 + ctx.font = overlayConfig.fontWidth + ' ' + fontSize + 'px ' + overlayConfig.font; 57 + 58 + ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; 59 + ctx.lineWidth = 2; 60 + 61 + ctx.textBaseline = 'middle'; 62 + ctx.textAlign = 'center'; 63 + 64 + ctx.strokeText(overlayConfig.text, rect.width / 2, rect.height / 2); 65 + 66 + ctx.globalCompositeOperation = 'destination-out'; 67 + ctx.fillText(overlayConfig.text, rect.width / 2, rect.height / 2); 68 + } 69 + 70 + // const input = document.getElementById('name'); 71 + 72 + // // on change 73 + // input.addEventListener('change', function() { 74 + // overlayConfig.text = input.value; 75 + // resizeOverlayCanvas(); 76 + // }); 77 + 78 + 79 + // Initial resize 80 + resizeOverlayCanvas(); 81 + 82 + // Resize canvas on window resize 83 + window.addEventListener('resize', resizeOverlayCanvas); 84 + 85 + 86 + // Simulation section 87 + 88 + const canvas = document.getElementsByTagName('canvas')[0]; 89 + resizeCanvas(); 90 + 91 + let config = { 92 + SIM_RESOLUTION: 128, 93 + DYE_RESOLUTION: 1024, 94 + CAPTURE_RESOLUTION: 512, 95 + DENSITY_DISSIPATION: 1.0, 96 + VELOCITY_DISSIPATION: 0.1, 97 + PRESSURE: 0.8, 98 + PRESSURE_ITERATIONS: 20, 99 + CURL: 30, 100 + SPLAT_RADIUS: 0.25, 101 + SPLAT_FORCE: 1000, 102 + SHADING: true, 103 + COLORFUL: true, 104 + COLOR_UPDATE_SPEED: 10, 105 + PAUSED: false, 106 + BACK_COLOR: { r: 0, g: 0, b: 0 }, 107 + TRANSPARENT: false, 108 + BLOOM: false, 109 + BLOOM_ITERATIONS: 8, 110 + BLOOM_RESOLUTION: 256, 111 + BLOOM_INTENSITY: 0.8, 112 + BLOOM_THRESHOLD: 0.8, 113 + BLOOM_SOFT_KNEE: 0.7, 114 + SUNRAYS: true, 115 + SUNRAYS_RESOLUTION: 196, 116 + SUNRAYS_WEIGHT: 1.0, 117 + START_HUE: 0.5, 118 + END_HUE: 1.0, 119 + RENDER_SPEED: 0.4 120 + }; 121 + 122 + function PointerPrototype() { 123 + this.id = -1; 124 + this.texcoordX = 0; 125 + this.texcoordY = 0; 126 + this.prevTexcoordX = 0; 127 + this.prevTexcoordY = 0; 128 + this.deltaX = 0; 129 + this.deltaY = 0; 130 + this.down = false; 131 + this.moved = false; 132 + this.color = [30, 0, 300]; 133 + } 134 + 135 + let pointers = []; 136 + let splatStack = []; 137 + pointers.push(new PointerPrototype()); 138 + 139 + const { gl, ext } = getWebGLContext(canvas); 140 + 141 + if (isMobile()) { 142 + config.DYE_RESOLUTION = 512; 143 + } 144 + if (!ext.supportLinearFiltering) { 145 + config.DYE_RESOLUTION = 512; 146 + config.SHADING = false; 147 + config.BLOOM = false; 148 + config.SUNRAYS = false; 149 + } 150 + 151 + function getWebGLContext(canvas) { 152 + const params = { 153 + alpha: true, 154 + depth: false, 155 + stencil: true, 156 + antialias: false, 157 + preserveDrawingBuffer: false 158 + }; 159 + 160 + 161 + let gl = canvas.getContext('webgl2', params); 162 + const isWebGL2 = !!gl; 163 + if (!isWebGL2) 164 + gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params); 165 + 166 + let halfFloat; 167 + let supportLinearFiltering; 168 + if (isWebGL2) { 169 + gl.getExtension('EXT_color_buffer_float'); 170 + supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); 171 + } else { 172 + halfFloat = gl.getExtension('OES_texture_half_float'); 173 + supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); 174 + } 175 + 176 + gl.clearColor(0.0, 0.0, 0.0, 1.0); 177 + 178 + const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; 179 + let formatRGBA; 180 + let formatRG; 181 + let formatR; 182 + 183 + if (isWebGL2) { 184 + formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); 185 + formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); 186 + formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); 187 + } else { 188 + formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 189 + formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 190 + formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); 191 + } 192 + 193 + return { 194 + gl, 195 + ext: { 196 + formatRGBA, 197 + formatRG, 198 + formatR, 199 + halfFloatTexType, 200 + supportLinearFiltering 201 + } 202 + }; 203 + } 204 + 205 + function getSupportedFormat(gl, internalFormat, format, type) { 206 + if (!supportRenderTextureFormat(gl, internalFormat, format, type)) { 207 + switch (internalFormat) { 208 + case gl.R16F: 209 + return getSupportedFormat(gl, gl.RG16F, gl.RG, type); 210 + case gl.RG16F: 211 + return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); 212 + default: 213 + return null; 214 + } 215 + } 216 + 217 + return { 218 + internalFormat, 219 + format 220 + }; 221 + } 222 + 223 + function supportRenderTextureFormat(gl, internalFormat, format, type) { 224 + let texture = gl.createTexture(); 225 + gl.bindTexture(gl.TEXTURE_2D, texture); 226 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 227 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 228 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 229 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 230 + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); 231 + 232 + let fbo = gl.createFramebuffer(); 233 + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 234 + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 235 + 236 + let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); 237 + return status == gl.FRAMEBUFFER_COMPLETE; 238 + } 239 + 240 + function isMobile() { 241 + return /Mobi|Android/i.test(navigator.userAgent); 242 + } 243 + 244 + class Material { 245 + constructor(vertexShader, fragmentShaderSource) { 246 + this.vertexShader = vertexShader; 247 + this.fragmentShaderSource = fragmentShaderSource; 248 + this.programs = []; 249 + this.activeProgram = null; 250 + this.uniforms = []; 251 + } 252 + 253 + setKeywords(keywords) { 254 + let hash = 0; 255 + for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]); 256 + 257 + let program = this.programs[hash]; 258 + if (program == null) { 259 + let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); 260 + program = createProgram(this.vertexShader, fragmentShader); 261 + this.programs[hash] = program; 262 + } 263 + 264 + if (program == this.activeProgram) return; 265 + 266 + this.uniforms = getUniforms(program); 267 + this.activeProgram = program; 268 + } 269 + 270 + bind() { 271 + gl.useProgram(this.activeProgram); 272 + } 273 + } 274 + 275 + class Program { 276 + constructor(vertexShader, fragmentShader) { 277 + this.uniforms = {}; 278 + this.program = createProgram(vertexShader, fragmentShader); 279 + this.uniforms = getUniforms(this.program); 280 + } 281 + 282 + bind() { 283 + gl.useProgram(this.program); 284 + } 285 + } 286 + 287 + function createProgram(vertexShader, fragmentShader) { 288 + let program = gl.createProgram(); 289 + gl.attachShader(program, vertexShader); 290 + gl.attachShader(program, fragmentShader); 291 + gl.linkProgram(program); 292 + 293 + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) 294 + console.trace(gl.getProgramInfoLog(program)); 295 + 296 + return program; 297 + } 298 + 299 + function getUniforms(program) { 300 + let uniforms = []; 301 + let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 302 + for (let i = 0; i < uniformCount; i++) { 303 + let uniformName = gl.getActiveUniform(program, i).name; 304 + uniforms[uniformName] = gl.getUniformLocation(program, uniformName); 305 + } 306 + return uniforms; 307 + } 308 + 309 + function compileShader(type, source, keywords) { 310 + source = addKeywords(source, keywords); 311 + 312 + const shader = gl.createShader(type); 313 + gl.shaderSource(shader, source); 314 + gl.compileShader(shader); 315 + 316 + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) console.trace(gl.getShaderInfoLog(shader)); 317 + 318 + return shader; 319 + } 320 + 321 + function addKeywords(source, keywords) { 322 + if (keywords == null) return source; 323 + let keywordsString = ''; 324 + keywords.forEach((keyword) => { 325 + keywordsString += '#define ' + keyword + '\n'; 326 + }); 327 + return keywordsString + source; 328 + } 329 + 330 + const baseVertexShader = compileShader( 331 + gl.VERTEX_SHADER, 332 + ` 333 + precision highp float; 334 + 335 + attribute vec2 aPosition; 336 + varying vec2 vUv; 337 + varying vec2 vL; 338 + varying vec2 vR; 339 + varying vec2 vT; 340 + varying vec2 vB; 341 + uniform vec2 texelSize; 342 + 343 + void main () { 344 + vUv = aPosition * 0.5 + 0.5; 345 + vL = vUv - vec2(texelSize.x, 0.0); 346 + vR = vUv + vec2(texelSize.x, 0.0); 347 + vT = vUv + vec2(0.0, texelSize.y); 348 + vB = vUv - vec2(0.0, texelSize.y); 349 + gl_Position = vec4(aPosition, 0.0, 1.0); 350 + } 351 + ` 352 + ); 353 + 354 + const blurVertexShader = compileShader( 355 + gl.VERTEX_SHADER, 356 + ` 357 + precision highp float; 358 + 359 + attribute vec2 aPosition; 360 + varying vec2 vUv; 361 + varying vec2 vL; 362 + varying vec2 vR; 363 + uniform vec2 texelSize; 364 + 365 + void main () { 366 + vUv = aPosition * 0.5 + 0.5; 367 + float offset = 1.33333333; 368 + vL = vUv - texelSize * offset; 369 + vR = vUv + texelSize * offset; 370 + gl_Position = vec4(aPosition, 0.0, 1.0); 371 + } 372 + ` 373 + ); 374 + 375 + const blurShader = compileShader( 376 + gl.FRAGMENT_SHADER, 377 + ` 378 + precision mediump float; 379 + precision mediump sampler2D; 380 + 381 + varying vec2 vUv; 382 + varying vec2 vL; 383 + varying vec2 vR; 384 + uniform sampler2D uTexture; 385 + 386 + void main () { 387 + vec4 sum = texture2D(uTexture, vUv) * 0.29411764; 388 + sum += texture2D(uTexture, vL) * 0.35294117; 389 + sum += texture2D(uTexture, vR) * 0.35294117; 390 + gl_FragColor = sum; 391 + } 392 + ` 393 + ); 394 + 395 + const copyShader = compileShader( 396 + gl.FRAGMENT_SHADER, 397 + ` 398 + precision mediump float; 399 + precision mediump sampler2D; 400 + 401 + varying highp vec2 vUv; 402 + uniform sampler2D uTexture; 403 + 404 + void main () { 405 + gl_FragColor = texture2D(uTexture, vUv); 406 + } 407 + ` 408 + ); 409 + 410 + const clearShader = compileShader( 411 + gl.FRAGMENT_SHADER, 412 + ` 413 + precision mediump float; 414 + precision mediump sampler2D; 415 + 416 + varying highp vec2 vUv; 417 + uniform sampler2D uTexture; 418 + uniform float value; 419 + 420 + void main () { 421 + gl_FragColor = value * texture2D(uTexture, vUv); 422 + } 423 + ` 424 + ); 425 + 426 + const colorShader = compileShader( 427 + gl.FRAGMENT_SHADER, 428 + ` 429 + precision mediump float; 430 + 431 + uniform vec4 color; 432 + 433 + void main () { 434 + gl_FragColor = color; 435 + } 436 + ` 437 + ); 438 + 439 + const checkerboardShader = compileShader( 440 + gl.FRAGMENT_SHADER, 441 + ` 442 + precision highp float; 443 + precision highp sampler2D; 444 + 445 + varying vec2 vUv; 446 + uniform sampler2D uTexture; 447 + uniform float aspectRatio; 448 + 449 + #define SCALE 25.0 450 + 451 + void main () { 452 + vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0)); 453 + float v = mod(uv.x + uv.y, 2.0); 454 + v = v * 0.1 + 0.8; 455 + gl_FragColor = vec4(vec3(v), 1.0); 456 + } 457 + ` 458 + ); 459 + 460 + const displayShaderSource = ` 461 + precision highp float; 462 + precision highp sampler2D; 463 + 464 + varying vec2 vUv; 465 + varying vec2 vL; 466 + varying vec2 vR; 467 + varying vec2 vT; 468 + varying vec2 vB; 469 + uniform sampler2D uTexture; 470 + uniform sampler2D uBloom; 471 + uniform sampler2D uSunrays; 472 + uniform sampler2D uDithering; 473 + uniform vec2 ditherScale; 474 + uniform vec2 texelSize; 475 + 476 + vec3 linearToGamma (vec3 color) { 477 + color = max(color, vec3(0)); 478 + return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); 479 + } 480 + 481 + void main () { 482 + vec3 c = texture2D(uTexture, vUv).rgb; 483 + 484 + #ifdef SHADING 485 + vec3 lc = texture2D(uTexture, vL).rgb; 486 + vec3 rc = texture2D(uTexture, vR).rgb; 487 + vec3 tc = texture2D(uTexture, vT).rgb; 488 + vec3 bc = texture2D(uTexture, vB).rgb; 489 + 490 + float dx = length(rc) - length(lc); 491 + float dy = length(tc) - length(bc); 492 + 493 + vec3 n = normalize(vec3(dx, dy, length(texelSize))); 494 + vec3 l = vec3(0.0, 0.0, 1.0); 495 + 496 + float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); 497 + c *= diffuse; 498 + #endif 499 + 500 + #ifdef BLOOM 501 + vec3 bloom = texture2D(uBloom, vUv).rgb; 502 + #endif 503 + 504 + #ifdef SUNRAYS 505 + float sunrays = texture2D(uSunrays, vUv).r; 506 + c *= sunrays; 507 + #ifdef BLOOM 508 + bloom *= sunrays; 509 + #endif 510 + #endif 511 + 512 + #ifdef BLOOM 513 + float noise = texture2D(uDithering, vUv * ditherScale).r; 514 + noise = noise * 2.0 - 1.0; 515 + bloom += noise / 255.0; 516 + bloom = linearToGamma(bloom); 517 + c += bloom; 518 + #endif 519 + 520 + float a = max(c.r, max(c.g, c.b)); 521 + gl_FragColor = vec4(c, a); 522 + } 523 + `; 524 + 525 + const bloomPrefilterShader = compileShader( 526 + gl.FRAGMENT_SHADER, 527 + ` 528 + precision mediump float; 529 + precision mediump sampler2D; 530 + 531 + varying vec2 vUv; 532 + uniform sampler2D uTexture; 533 + uniform vec3 curve; 534 + uniform float threshold; 535 + 536 + void main () { 537 + vec3 c = texture2D(uTexture, vUv).rgb; 538 + float br = max(c.r, max(c.g, c.b)); 539 + float rq = clamp(br - curve.x, 0.0, curve.y); 540 + rq = curve.z * rq * rq; 541 + c *= max(rq, br - threshold) / max(br, 0.0001); 542 + gl_FragColor = vec4(c, 0.0); 543 + } 544 + ` 545 + ); 546 + 547 + const bloomBlurShader = compileShader( 548 + gl.FRAGMENT_SHADER, 549 + ` 550 + precision mediump float; 551 + precision mediump sampler2D; 552 + 553 + varying vec2 vL; 554 + varying vec2 vR; 555 + varying vec2 vT; 556 + varying vec2 vB; 557 + uniform sampler2D uTexture; 558 + 559 + void main () { 560 + vec4 sum = vec4(0.0); 561 + sum += texture2D(uTexture, vL); 562 + sum += texture2D(uTexture, vR); 563 + sum += texture2D(uTexture, vT); 564 + sum += texture2D(uTexture, vB); 565 + sum *= 0.25; 566 + gl_FragColor = sum; 567 + } 568 + ` 569 + ); 570 + 571 + const bloomFinalShader = compileShader( 572 + gl.FRAGMENT_SHADER, 573 + ` 574 + precision mediump float; 575 + precision mediump sampler2D; 576 + 577 + varying vec2 vL; 578 + varying vec2 vR; 579 + varying vec2 vT; 580 + varying vec2 vB; 581 + uniform sampler2D uTexture; 582 + uniform float intensity; 583 + 584 + void main () { 585 + vec4 sum = vec4(0.0); 586 + sum += texture2D(uTexture, vL); 587 + sum += texture2D(uTexture, vR); 588 + sum += texture2D(uTexture, vT); 589 + sum += texture2D(uTexture, vB); 590 + sum *= 0.25; 591 + gl_FragColor = sum * intensity; 592 + } 593 + ` 594 + ); 595 + 596 + const sunraysMaskShader = compileShader( 597 + gl.FRAGMENT_SHADER, 598 + ` 599 + precision highp float; 600 + precision highp sampler2D; 601 + 602 + varying vec2 vUv; 603 + uniform sampler2D uTexture; 604 + 605 + void main () { 606 + vec4 c = texture2D(uTexture, vUv); 607 + float br = max(c.r, max(c.g, c.b)); 608 + c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8); 609 + gl_FragColor = c; 610 + } 611 + ` 612 + ); 613 + 614 + const sunraysShader = compileShader( 615 + gl.FRAGMENT_SHADER, 616 + ` 617 + precision highp float; 618 + precision highp sampler2D; 619 + 620 + varying vec2 vUv; 621 + uniform sampler2D uTexture; 622 + uniform float weight; 623 + 624 + #define ITERATIONS 16 625 + 626 + void main () { 627 + float Density = 0.3; 628 + float Decay = 0.95; 629 + float Exposure = 0.7; 630 + 631 + vec2 coord = vUv; 632 + vec2 dir = vUv - 0.5; 633 + 634 + dir *= 1.0 / float(ITERATIONS) * Density; 635 + float illuminationDecay = 1.0; 636 + 637 + float color = texture2D(uTexture, vUv).a; 638 + 639 + for (int i = 0; i < ITERATIONS; i++) 640 + { 641 + coord -= dir; 642 + float col = texture2D(uTexture, coord).a; 643 + color += col * illuminationDecay * weight; 644 + illuminationDecay *= Decay; 645 + } 646 + 647 + gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0); 648 + } 649 + ` 650 + ); 651 + 652 + const splatShader = compileShader( 653 + gl.FRAGMENT_SHADER, 654 + ` 655 + precision highp float; 656 + precision highp sampler2D; 657 + 658 + varying vec2 vUv; 659 + uniform sampler2D uTarget; 660 + uniform float aspectRatio; 661 + uniform vec3 color; 662 + uniform vec2 point; 663 + uniform float radius; 664 + 665 + void main () { 666 + vec2 p = vUv - point.xy; 667 + p.x *= aspectRatio; 668 + vec3 splat = exp(-dot(p, p) / radius) * color; 669 + vec3 base = texture2D(uTarget, vUv).xyz; 670 + gl_FragColor = vec4(base + splat, 1.0); 671 + } 672 + ` 673 + ); 674 + 675 + const advectionShader = compileShader( 676 + gl.FRAGMENT_SHADER, 677 + ` 678 + precision highp float; 679 + precision highp sampler2D; 680 + 681 + varying vec2 vUv; 682 + uniform sampler2D uVelocity; 683 + uniform sampler2D uSource; 684 + uniform vec2 texelSize; 685 + uniform vec2 dyeTexelSize; 686 + uniform float dt; 687 + uniform float dissipation; 688 + 689 + vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { 690 + vec2 st = uv / tsize - 0.5; 691 + 692 + vec2 iuv = floor(st); 693 + vec2 fuv = fract(st); 694 + 695 + vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); 696 + vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); 697 + vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); 698 + vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); 699 + 700 + return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); 701 + } 702 + 703 + void main () { 704 + #ifdef MANUAL_FILTERING 705 + vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; 706 + vec4 result = bilerp(uSource, coord, dyeTexelSize); 707 + #else 708 + vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; 709 + vec4 result = texture2D(uSource, coord); 710 + #endif 711 + float decay = 1.0 + dissipation * dt; 712 + gl_FragColor = result / decay; 713 + }`, 714 + ext.supportLinearFiltering ? null : ['MANUAL_FILTERING'] 715 + ); 716 + 717 + const divergenceShader = compileShader( 718 + gl.FRAGMENT_SHADER, 719 + ` 720 + precision mediump float; 721 + precision mediump sampler2D; 722 + 723 + varying highp vec2 vUv; 724 + varying highp vec2 vL; 725 + varying highp vec2 vR; 726 + varying highp vec2 vT; 727 + varying highp vec2 vB; 728 + uniform sampler2D uVelocity; 729 + 730 + void main () { 731 + float L = texture2D(uVelocity, vL).x; 732 + float R = texture2D(uVelocity, vR).x; 733 + float T = texture2D(uVelocity, vT).y; 734 + float B = texture2D(uVelocity, vB).y; 735 + 736 + vec2 C = texture2D(uVelocity, vUv).xy; 737 + if (vL.x < 0.0) { L = -C.x; } 738 + if (vR.x > 1.0) { R = -C.x; } 739 + if (vT.y > 1.0) { T = -C.y; } 740 + if (vB.y < 0.0) { B = -C.y; } 741 + 742 + float div = 0.5 * (R - L + T - B); 743 + gl_FragColor = vec4(div, 0.0, 0.0, 1.0); 744 + } 745 + ` 746 + ); 747 + 748 + const curlShader = compileShader( 749 + gl.FRAGMENT_SHADER, 750 + ` 751 + precision mediump float; 752 + precision mediump sampler2D; 753 + 754 + varying highp vec2 vUv; 755 + varying highp vec2 vL; 756 + varying highp vec2 vR; 757 + varying highp vec2 vT; 758 + varying highp vec2 vB; 759 + uniform sampler2D uVelocity; 760 + 761 + void main () { 762 + float L = texture2D(uVelocity, vL).y; 763 + float R = texture2D(uVelocity, vR).y; 764 + float T = texture2D(uVelocity, vT).x; 765 + float B = texture2D(uVelocity, vB).x; 766 + float vorticity = R - L - T + B; 767 + gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); 768 + } 769 + ` 770 + ); 771 + 772 + const vorticityShader = compileShader( 773 + gl.FRAGMENT_SHADER, 774 + ` 775 + precision highp float; 776 + precision highp sampler2D; 777 + 778 + varying vec2 vUv; 779 + varying vec2 vL; 780 + varying vec2 vR; 781 + varying vec2 vT; 782 + varying vec2 vB; 783 + uniform sampler2D uVelocity; 784 + uniform sampler2D uCurl; 785 + uniform float curl; 786 + uniform float dt; 787 + 788 + void main () { 789 + float L = texture2D(uCurl, vL).x; 790 + float R = texture2D(uCurl, vR).x; 791 + float T = texture2D(uCurl, vT).x; 792 + float B = texture2D(uCurl, vB).x; 793 + float C = texture2D(uCurl, vUv).x; 794 + 795 + vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); 796 + force /= length(force) + 0.0001; 797 + force *= curl * C; 798 + force.y *= -1.0; 799 + 800 + vec2 velocity = texture2D(uVelocity, vUv).xy; 801 + velocity += force * dt; 802 + velocity = min(max(velocity, -1000.0), 1000.0); 803 + gl_FragColor = vec4(velocity, 0.0, 1.0); 804 + } 805 + ` 806 + ); 807 + 808 + const pressureShader = compileShader( 809 + gl.FRAGMENT_SHADER, 810 + ` 811 + precision mediump float; 812 + precision mediump sampler2D; 813 + 814 + varying highp vec2 vUv; 815 + varying highp vec2 vL; 816 + varying highp vec2 vR; 817 + varying highp vec2 vT; 818 + varying highp vec2 vB; 819 + uniform sampler2D uPressure; 820 + uniform sampler2D uDivergence; 821 + 822 + void main () { 823 + float L = texture2D(uPressure, vL).x; 824 + float R = texture2D(uPressure, vR).x; 825 + float T = texture2D(uPressure, vT).x; 826 + float B = texture2D(uPressure, vB).x; 827 + float C = texture2D(uPressure, vUv).x; 828 + float divergence = texture2D(uDivergence, vUv).x; 829 + float pressure = (L + R + B + T - divergence) * 0.25; 830 + gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); 831 + } 832 + ` 833 + ); 834 + 835 + const gradientSubtractShader = compileShader( 836 + gl.FRAGMENT_SHADER, 837 + ` 838 + precision mediump float; 839 + precision mediump sampler2D; 840 + 841 + varying highp vec2 vUv; 842 + varying highp vec2 vL; 843 + varying highp vec2 vR; 844 + varying highp vec2 vT; 845 + varying highp vec2 vB; 846 + uniform sampler2D uPressure; 847 + uniform sampler2D uVelocity; 848 + 849 + void main () { 850 + float L = texture2D(uPressure, vL).x; 851 + float R = texture2D(uPressure, vR).x; 852 + float T = texture2D(uPressure, vT).x; 853 + float B = texture2D(uPressure, vB).x; 854 + vec2 velocity = texture2D(uVelocity, vUv).xy; 855 + velocity.xy -= vec2(R - L, T - B); 856 + gl_FragColor = vec4(velocity, 0.0, 1.0); 857 + } 858 + ` 859 + ); 860 + 861 + const blit = (() => { 862 + gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); 863 + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); 864 + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); 865 + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); 866 + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); 867 + gl.enableVertexAttribArray(0); 868 + 869 + return (target, clear = false) => { 870 + if (target == null) { 871 + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 872 + gl.bindFramebuffer(gl.FRAMEBUFFER, null); 873 + } else { 874 + gl.viewport(0, 0, target.width, target.height); 875 + gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); 876 + } 877 + if (clear) { 878 + gl.clearColor(0.0, 0.0, 0.0, 1.0); 879 + gl.clear(gl.COLOR_BUFFER_BIT); 880 + } 881 + // CHECK_FRAMEBUFFER_STATUS(); 882 + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); 883 + }; 884 + })(); 885 + 886 + function CHECK_FRAMEBUFFER_STATUS() { 887 + let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); 888 + if (status != gl.FRAMEBUFFER_COMPLETE) console.trace('Framebuffer error: ' + status); 889 + } 890 + 891 + let dye; 892 + let velocity; 893 + let divergence; 894 + let curl; 895 + let pressure; 896 + let bloom; 897 + let bloomFramebuffers = []; 898 + let sunrays; 899 + let sunraysTemp; 900 + 901 + let ditheringTexture = createTextureAsync('LDR_LLL1_0.png'); 902 + 903 + const blurProgram = new Program(blurVertexShader, blurShader); 904 + const copyProgram = new Program(baseVertexShader, copyShader); 905 + const clearProgram = new Program(baseVertexShader, clearShader); 906 + const colorProgram = new Program(baseVertexShader, colorShader); 907 + const checkerboardProgram = new Program(baseVertexShader, checkerboardShader); 908 + const bloomPrefilterProgram = new Program(baseVertexShader, bloomPrefilterShader); 909 + const bloomBlurProgram = new Program(baseVertexShader, bloomBlurShader); 910 + const bloomFinalProgram = new Program(baseVertexShader, bloomFinalShader); 911 + const sunraysMaskProgram = new Program(baseVertexShader, sunraysMaskShader); 912 + const sunraysProgram = new Program(baseVertexShader, sunraysShader); 913 + const splatProgram = new Program(baseVertexShader, splatShader); 914 + const advectionProgram = new Program(baseVertexShader, advectionShader); 915 + const divergenceProgram = new Program(baseVertexShader, divergenceShader); 916 + const curlProgram = new Program(baseVertexShader, curlShader); 917 + const vorticityProgram = new Program(baseVertexShader, vorticityShader); 918 + const pressureProgram = new Program(baseVertexShader, pressureShader); 919 + const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); 920 + 921 + const displayMaterial = new Material(baseVertexShader, displayShaderSource); 922 + 923 + function initFramebuffers() { 924 + let simRes = getResolution(config.SIM_RESOLUTION); 925 + let dyeRes = getResolution(config.DYE_RESOLUTION); 926 + 927 + const texType = ext.halfFloatTexType; 928 + const rgba = ext.formatRGBA; 929 + const rg = ext.formatRG; 930 + const r = ext.formatR; 931 + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; 932 + 933 + gl.disable(gl.BLEND); 934 + 935 + if (dye == null) 936 + dye = createDoubleFBO( 937 + dyeRes.width, 938 + dyeRes.height, 939 + rgba.internalFormat, 940 + rgba.format, 941 + texType, 942 + filtering 943 + ); 944 + else 945 + dye = resizeDoubleFBO( 946 + dye, 947 + dyeRes.width, 948 + dyeRes.height, 949 + rgba.internalFormat, 950 + rgba.format, 951 + texType, 952 + filtering 953 + ); 954 + 955 + if (velocity == null) 956 + velocity = createDoubleFBO( 957 + simRes.width, 958 + simRes.height, 959 + rg.internalFormat, 960 + rg.format, 961 + texType, 962 + filtering 963 + ); 964 + else 965 + velocity = resizeDoubleFBO( 966 + velocity, 967 + simRes.width, 968 + simRes.height, 969 + rg.internalFormat, 970 + rg.format, 971 + texType, 972 + filtering 973 + ); 974 + 975 + divergence = createFBO( 976 + simRes.width, 977 + simRes.height, 978 + r.internalFormat, 979 + r.format, 980 + texType, 981 + gl.NEAREST 982 + ); 983 + curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); 984 + pressure = createDoubleFBO( 985 + simRes.width, 986 + simRes.height, 987 + r.internalFormat, 988 + r.format, 989 + texType, 990 + gl.NEAREST 991 + ); 992 + 993 + initBloomFramebuffers(); 994 + initSunraysFramebuffers(); 995 + } 996 + 997 + function initBloomFramebuffers() { 998 + let res = getResolution(config.BLOOM_RESOLUTION); 999 + 1000 + const texType = ext.halfFloatTexType; 1001 + const rgba = ext.formatRGBA; 1002 + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; 1003 + 1004 + bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering); 1005 + 1006 + bloomFramebuffers.length = 0; 1007 + for (let i = 0; i < config.BLOOM_ITERATIONS; i++) { 1008 + let width = res.width >> (i + 1); 1009 + let height = res.height >> (i + 1); 1010 + 1011 + if (width < 2 || height < 2) break; 1012 + 1013 + let fbo = createFBO(width, height, rgba.internalFormat, rgba.format, texType, filtering); 1014 + bloomFramebuffers.push(fbo); 1015 + } 1016 + } 1017 + 1018 + function initSunraysFramebuffers() { 1019 + let res = getResolution(config.SUNRAYS_RESOLUTION); 1020 + 1021 + const texType = ext.halfFloatTexType; 1022 + const r = ext.formatR; 1023 + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; 1024 + 1025 + sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); 1026 + sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); 1027 + } 1028 + 1029 + function createFBO(w, h, internalFormat, format, type, param) { 1030 + gl.activeTexture(gl.TEXTURE0); 1031 + let texture = gl.createTexture(); 1032 + gl.bindTexture(gl.TEXTURE_2D, texture); 1033 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); 1034 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); 1035 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 1036 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 1037 + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); 1038 + 1039 + let fbo = gl.createFramebuffer(); 1040 + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 1041 + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 1042 + gl.viewport(0, 0, w, h); 1043 + gl.clear(gl.COLOR_BUFFER_BIT); 1044 + 1045 + let texelSizeX = 1.0 / w; 1046 + let texelSizeY = 1.0 / h; 1047 + 1048 + return { 1049 + texture, 1050 + fbo, 1051 + width: w, 1052 + height: h, 1053 + texelSizeX, 1054 + texelSizeY, 1055 + attach(id) { 1056 + gl.activeTexture(gl.TEXTURE0 + id); 1057 + gl.bindTexture(gl.TEXTURE_2D, texture); 1058 + return id; 1059 + } 1060 + }; 1061 + } 1062 + 1063 + function createDoubleFBO(w, h, internalFormat, format, type, param) { 1064 + let fbo1 = createFBO(w, h, internalFormat, format, type, param); 1065 + let fbo2 = createFBO(w, h, internalFormat, format, type, param); 1066 + 1067 + return { 1068 + width: w, 1069 + height: h, 1070 + texelSizeX: fbo1.texelSizeX, 1071 + texelSizeY: fbo1.texelSizeY, 1072 + get read() { 1073 + return fbo1; 1074 + }, 1075 + set read(value) { 1076 + fbo1 = value; 1077 + }, 1078 + get write() { 1079 + return fbo2; 1080 + }, 1081 + set write(value) { 1082 + fbo2 = value; 1083 + }, 1084 + swap() { 1085 + let temp = fbo1; 1086 + fbo1 = fbo2; 1087 + fbo2 = temp; 1088 + } 1089 + }; 1090 + } 1091 + 1092 + function resizeFBO(target, w, h, internalFormat, format, type, param) { 1093 + let newFBO = createFBO(w, h, internalFormat, format, type, param); 1094 + copyProgram.bind(); 1095 + gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); 1096 + blit(newFBO); 1097 + return newFBO; 1098 + } 1099 + 1100 + function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) { 1101 + if (target.width == w && target.height == h) return target; 1102 + target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); 1103 + target.write = createFBO(w, h, internalFormat, format, type, param); 1104 + target.width = w; 1105 + target.height = h; 1106 + target.texelSizeX = 1.0 / w; 1107 + target.texelSizeY = 1.0 / h; 1108 + return target; 1109 + } 1110 + 1111 + function createTextureAsync(url) { 1112 + let texture = gl.createTexture(); 1113 + gl.bindTexture(gl.TEXTURE_2D, texture); 1114 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 1115 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 1116 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); 1117 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); 1118 + gl.texImage2D( 1119 + gl.TEXTURE_2D, 1120 + 0, 1121 + gl.RGB, 1122 + 1, 1123 + 1, 1124 + 0, 1125 + gl.RGB, 1126 + gl.UNSIGNED_BYTE, 1127 + new Uint8Array([255, 255, 255]) 1128 + ); 1129 + 1130 + let obj = { 1131 + texture, 1132 + width: 1, 1133 + height: 1, 1134 + attach(id) { 1135 + gl.activeTexture(gl.TEXTURE0 + id); 1136 + gl.bindTexture(gl.TEXTURE_2D, texture); 1137 + return id; 1138 + } 1139 + }; 1140 + 1141 + let image = new Image(); 1142 + image.onload = () => { 1143 + obj.width = image.width; 1144 + obj.height = image.height; 1145 + gl.bindTexture(gl.TEXTURE_2D, texture); 1146 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 1147 + }; 1148 + image.src = url; 1149 + 1150 + return obj; 1151 + } 1152 + 1153 + function updateKeywords() { 1154 + let displayKeywords = []; 1155 + if (config.SHADING) displayKeywords.push('SHADING'); 1156 + if (config.BLOOM) displayKeywords.push('BLOOM'); 1157 + if (config.SUNRAYS) displayKeywords.push('SUNRAYS'); 1158 + displayMaterial.setKeywords(displayKeywords); 1159 + } 1160 + 1161 + updateKeywords(); 1162 + initFramebuffers(); 1163 + multipleSplats(25); 1164 + 1165 + let lastUpdateTime = Date.now(); 1166 + let colorUpdateTimer = 0.0; 1167 + update(); 1168 + 1169 + function update() { 1170 + const dt = calcDeltaTime() * (config.RENDER_SPEED ?? 1.0); 1171 + if (resizeCanvas()) initFramebuffers(); 1172 + updateColors(dt); 1173 + applyInputs(); 1174 + if (!config.PAUSED) step(dt); 1175 + render(null); 1176 + requestAnimationFrame(update); 1177 + } 1178 + 1179 + function calcDeltaTime() { 1180 + let now = Date.now(); 1181 + let dt = (now - lastUpdateTime) / 1000; 1182 + dt = Math.min(dt, 0.016666); 1183 + lastUpdateTime = now; 1184 + return dt; 1185 + } 1186 + 1187 + function resizeCanvas() { 1188 + let width = scaleByPixelRatio(canvas.clientWidth); 1189 + let height = scaleByPixelRatio(canvas.clientHeight); 1190 + if (canvas.width != width || canvas.height != height) { 1191 + canvas.width = width; 1192 + canvas.height = height; 1193 + return true; 1194 + } 1195 + return false; 1196 + } 1197 + 1198 + function updateColors(dt) { 1199 + if (!config.COLORFUL) return; 1200 + 1201 + colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; 1202 + if (colorUpdateTimer >= 1) { 1203 + colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); 1204 + pointers.forEach((p) => { 1205 + p.color = generateColor(); 1206 + }); 1207 + } 1208 + } 1209 + 1210 + function applyInputs() { 1211 + if (splatStack.length > 0) multipleSplats(splatStack.pop()); 1212 + 1213 + pointers.forEach((p) => { 1214 + if (p.moved) { 1215 + p.moved = false; 1216 + splatPointer(p); 1217 + } 1218 + }); 1219 + } 1220 + 1221 + function step(dt) { 1222 + gl.disable(gl.BLEND); 1223 + 1224 + curlProgram.bind(); 1225 + gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1226 + gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); 1227 + blit(curl); 1228 + 1229 + vorticityProgram.bind(); 1230 + gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1231 + gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); 1232 + gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); 1233 + gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); 1234 + gl.uniform1f(vorticityProgram.uniforms.dt, dt); 1235 + blit(velocity.write); 1236 + velocity.swap(); 1237 + 1238 + divergenceProgram.bind(); 1239 + gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1240 + gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); 1241 + blit(divergence); 1242 + 1243 + clearProgram.bind(); 1244 + gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); 1245 + gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); 1246 + blit(pressure.write); 1247 + pressure.swap(); 1248 + 1249 + pressureProgram.bind(); 1250 + gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1251 + gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); 1252 + for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { 1253 + gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); 1254 + blit(pressure.write); 1255 + pressure.swap(); 1256 + } 1257 + 1258 + gradienSubtractProgram.bind(); 1259 + gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1260 + gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); 1261 + gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); 1262 + blit(velocity.write); 1263 + velocity.swap(); 1264 + 1265 + advectionProgram.bind(); 1266 + gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); 1267 + if (!ext.supportLinearFiltering) 1268 + gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); 1269 + let velocityId = velocity.read.attach(0); 1270 + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); 1271 + gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); 1272 + gl.uniform1f(advectionProgram.uniforms.dt, dt); 1273 + gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); 1274 + blit(velocity.write); 1275 + velocity.swap(); 1276 + 1277 + if (!ext.supportLinearFiltering) 1278 + gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); 1279 + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); 1280 + gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); 1281 + gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); 1282 + blit(dye.write); 1283 + dye.swap(); 1284 + } 1285 + 1286 + function render(target) { 1287 + if (config.BLOOM) applyBloom(dye.read, bloom); 1288 + if (config.SUNRAYS) { 1289 + applySunrays(dye.read, dye.write, sunrays); 1290 + blur(sunrays, sunraysTemp, 1); 1291 + } 1292 + 1293 + if (target == null || !config.TRANSPARENT) { 1294 + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 1295 + gl.enable(gl.BLEND); 1296 + } else { 1297 + gl.disable(gl.BLEND); 1298 + } 1299 + 1300 + if (!config.TRANSPARENT) drawColor(target, normalizeColor(config.BACK_COLOR)); 1301 + if (target == null && config.TRANSPARENT) drawCheckerboard(target); 1302 + drawDisplay(target); 1303 + } 1304 + 1305 + function drawColor(target, color) { 1306 + colorProgram.bind(); 1307 + gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1); 1308 + blit(target); 1309 + } 1310 + 1311 + function drawCheckerboard(target) { 1312 + checkerboardProgram.bind(); 1313 + gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height); 1314 + blit(target); 1315 + } 1316 + 1317 + function drawDisplay(target) { 1318 + let width = target == null ? gl.drawingBufferWidth : target.width; 1319 + let height = target == null ? gl.drawingBufferHeight : target.height; 1320 + 1321 + displayMaterial.bind(); 1322 + if (config.SHADING) gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); 1323 + gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); 1324 + if (config.BLOOM) { 1325 + gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1)); 1326 + gl.uniform1i(displayMaterial.uniforms.uDithering, ditheringTexture.attach(2)); 1327 + let scale = getTextureScale(ditheringTexture, width, height); 1328 + gl.uniform2f(displayMaterial.uniforms.ditherScale, scale.x, scale.y); 1329 + } 1330 + if (config.SUNRAYS) gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)); 1331 + blit(target); 1332 + } 1333 + 1334 + function applyBloom(source, destination) { 1335 + if (bloomFramebuffers.length < 2) return; 1336 + 1337 + let last = destination; 1338 + 1339 + gl.disable(gl.BLEND); 1340 + bloomPrefilterProgram.bind(); 1341 + let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001; 1342 + let curve0 = config.BLOOM_THRESHOLD - knee; 1343 + let curve1 = knee * 2; 1344 + let curve2 = 0.25 / knee; 1345 + gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2); 1346 + gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD); 1347 + gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0)); 1348 + blit(last); 1349 + 1350 + bloomBlurProgram.bind(); 1351 + for (let i = 0; i < bloomFramebuffers.length; i++) { 1352 + let dest = bloomFramebuffers[i]; 1353 + gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); 1354 + gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); 1355 + blit(dest); 1356 + last = dest; 1357 + } 1358 + 1359 + gl.blendFunc(gl.ONE, gl.ONE); 1360 + gl.enable(gl.BLEND); 1361 + 1362 + for (let i = bloomFramebuffers.length - 2; i >= 0; i--) { 1363 + let baseTex = bloomFramebuffers[i]; 1364 + gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); 1365 + gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); 1366 + gl.viewport(0, 0, baseTex.width, baseTex.height); 1367 + blit(baseTex); 1368 + last = baseTex; 1369 + } 1370 + 1371 + gl.disable(gl.BLEND); 1372 + bloomFinalProgram.bind(); 1373 + gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); 1374 + gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0)); 1375 + gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY); 1376 + blit(destination); 1377 + } 1378 + 1379 + function applySunrays(source, mask, destination) { 1380 + gl.disable(gl.BLEND); 1381 + sunraysMaskProgram.bind(); 1382 + gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)); 1383 + blit(mask); 1384 + 1385 + sunraysProgram.bind(); 1386 + gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT); 1387 + gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)); 1388 + blit(destination); 1389 + } 1390 + 1391 + function blur(target, temp, iterations) { 1392 + blurProgram.bind(); 1393 + for (let i = 0; i < iterations; i++) { 1394 + gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0); 1395 + gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)); 1396 + blit(temp); 1397 + 1398 + gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY); 1399 + gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)); 1400 + blit(target); 1401 + } 1402 + } 1403 + 1404 + function splatPointer(pointer) { 1405 + let dx = pointer.deltaX * config.SPLAT_FORCE * 5; 1406 + let dy = pointer.deltaY * config.SPLAT_FORCE * 5; 1407 + splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); 1408 + } 1409 + 1410 + function multipleSplats(amount) { 1411 + for (let i = 0; i < amount; i++) { 1412 + const color = generateColor(); 1413 + color.r *= 10.0; 1414 + color.g *= 10.0; 1415 + color.b *= 10.0; 1416 + const x = Math.random(); 1417 + const y = Math.random() < 0.5 ? 0.8 : 0.2; 1418 + const dx = 100 * (Math.random() - 0.5); 1419 + const dy = 1000 * (Math.random() - 0.5); 1420 + splat(x, y, dx, dy, color); 1421 + } 1422 + } 1423 + 1424 + function splat(x, y, dx, dy, color) { 1425 + splatProgram.bind(); 1426 + gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); 1427 + gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); 1428 + gl.uniform2f(splatProgram.uniforms.point, x, y); 1429 + gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); 1430 + gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); 1431 + blit(velocity.write); 1432 + velocity.swap(); 1433 + 1434 + gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); 1435 + gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); 1436 + blit(dye.write); 1437 + dye.swap(); 1438 + } 1439 + 1440 + function correctRadius(radius) { 1441 + let aspectRatio = canvas.width / canvas.height; 1442 + if (aspectRatio > 1) radius *= aspectRatio; 1443 + return radius; 1444 + } 1445 + 1446 + canvas.addEventListener('mousedown', (e) => { 1447 + let posX = scaleByPixelRatio(e.offsetX); 1448 + let posY = scaleByPixelRatio(e.offsetY); 1449 + let pointer = pointers.find((p) => p.id == -1); 1450 + if (pointer == null) pointer = new PointerPrototype(); 1451 + updatePointerDownData(pointer, -1, posX, posY); 1452 + }); 1453 + 1454 + document.body.addEventListener('mousemove', (e) => { 1455 + let pointer = pointers[0]; 1456 + // if (!pointer.down) return; 1457 + let posX = scaleByPixelRatio(e.offsetX); 1458 + let posY = scaleByPixelRatio(e.offsetY); 1459 + updatePointerMoveData(pointer, posX, posY); 1460 + }); 1461 + 1462 + window.addEventListener('mouseup', () => { 1463 + updatePointerUpData(pointers[0]); 1464 + }); 1465 + 1466 + document.body.addEventListener('touchstart', (e) => { 1467 + e.preventDefault(); 1468 + const touches = e.targetTouches; 1469 + while (touches.length >= pointers.length) pointers.push(new PointerPrototype()); 1470 + for (let i = 0; i < touches.length; i++) { 1471 + let posX = scaleByPixelRatio(touches[i].pageX); 1472 + let posY = scaleByPixelRatio(touches[i].pageY); 1473 + updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); 1474 + } 1475 + }); 1476 + 1477 + document.body.addEventListener( 1478 + 'touchmove', 1479 + (e) => { 1480 + e.preventDefault(); 1481 + const touches = e.targetTouches; 1482 + for (let i = 0; i < touches.length; i++) { 1483 + let pointer = pointers[i + 1]; 1484 + if (!pointer.down) continue; 1485 + let posX = scaleByPixelRatio(touches[i].pageX); 1486 + let posY = scaleByPixelRatio(touches[i].pageY); 1487 + updatePointerMoveData(pointer, posX, posY); 1488 + } 1489 + }, 1490 + false 1491 + ); 1492 + 1493 + document.body.addEventListener('touchend', (e) => { 1494 + const touches = e.changedTouches; 1495 + for (const element of touches) { 1496 + let pointer = pointers.find((p) => p.id == element.identifier); 1497 + if (pointer == null) continue; 1498 + updatePointerUpData(pointer); 1499 + } 1500 + }); 1501 + 1502 + // window.addEventListener('keydown', (e) => { 1503 + // if (e.code === 'KeyP') config.PAUSED = !config.PAUSED; 1504 + // if (e.key === ' ') splatStack.push(parseInt(Math.random() * 20) + 5); 1505 + // }); 1506 + 1507 + function updatePointerDownData(pointer, id, posX, posY) { 1508 + pointer.id = id; 1509 + pointer.down = true; 1510 + pointer.moved = false; 1511 + pointer.texcoordX = posX / canvas.width; 1512 + pointer.texcoordY = 1.0 - posY / canvas.height; 1513 + pointer.prevTexcoordX = pointer.texcoordX; 1514 + pointer.prevTexcoordY = pointer.texcoordY; 1515 + pointer.deltaX = 0; 1516 + pointer.deltaY = 0; 1517 + pointer.color = generateColor(); 1518 + } 1519 + 1520 + function updatePointerMoveData(pointer, posX, posY) { 1521 + pointer.prevTexcoordX = pointer.texcoordX; 1522 + pointer.prevTexcoordY = pointer.texcoordY; 1523 + pointer.texcoordX = posX / canvas.width; 1524 + pointer.texcoordY = 1.0 - posY / canvas.height; 1525 + pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); 1526 + pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); 1527 + pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; 1528 + } 1529 + 1530 + function updatePointerUpData(pointer) { 1531 + pointer.down = false; 1532 + } 1533 + 1534 + function correctDeltaX(delta) { 1535 + let aspectRatio = canvas.width / canvas.height; 1536 + if (aspectRatio < 1) delta *= aspectRatio; 1537 + return delta; 1538 + } 1539 + 1540 + function correctDeltaY(delta) { 1541 + let aspectRatio = canvas.width / canvas.height; 1542 + if (aspectRatio > 1) delta /= aspectRatio; 1543 + return delta; 1544 + } 1545 + 1546 + function generateColor() { 1547 + let c = HSVtoRGB(Math.random() * (config.END_HUE - config.START_HUE) + config.START_HUE, 1.0, 1.0); 1548 + c.r *= 0.15; 1549 + c.g *= 0.15; 1550 + c.b *= 0.15; 1551 + return c; 1552 + } 1553 + 1554 + function HSVtoRGB(h, s, v) { 1555 + let r, g, b, i, f, p, q, t; 1556 + i = Math.floor(h * 6); 1557 + f = h * 6 - i; 1558 + p = v * (1 - s); 1559 + q = v * (1 - f * s); 1560 + t = v * (1 - (1 - f) * s); 1561 + 1562 + switch (i % 6) { 1563 + case 0: 1564 + (r = v), (g = t), (b = p); 1565 + break; 1566 + case 1: 1567 + (r = q), (g = v), (b = p); 1568 + break; 1569 + case 2: 1570 + (r = p), (g = v), (b = t); 1571 + break; 1572 + case 3: 1573 + (r = p), (g = q), (b = v); 1574 + break; 1575 + case 4: 1576 + (r = t), (g = p), (b = v); 1577 + break; 1578 + case 5: 1579 + (r = v), (g = p), (b = q); 1580 + break; 1581 + } 1582 + 1583 + return { 1584 + r, 1585 + g, 1586 + b 1587 + }; 1588 + } 1589 + 1590 + function normalizeColor(input) { 1591 + let output = { 1592 + r: input.r / 255, 1593 + g: input.g / 255, 1594 + b: input.b / 255 1595 + }; 1596 + return output; 1597 + } 1598 + 1599 + function wrap(value, min, max) { 1600 + let range = max - min; 1601 + if (range == 0) return min; 1602 + return ((value - min) % range) + min; 1603 + } 1604 + 1605 + function getResolution(resolution) { 1606 + let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; 1607 + if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio; 1608 + 1609 + let min = Math.round(resolution); 1610 + let max = Math.round(resolution * aspectRatio); 1611 + 1612 + if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min }; 1613 + else return { width: min, height: max }; 1614 + } 1615 + 1616 + function getTextureScale(texture, width, height) { 1617 + return { 1618 + x: width / texture.width, 1619 + y: height / texture.height 1620 + }; 1621 + } 1622 + 1623 + function scaleByPixelRatio(input) { 1624 + let pixelRatio = window.devicePixelRatio || 1; 1625 + return Math.floor(input * pixelRatio); 1626 + } 1627 + 1628 + function hashCode(s) { 1629 + if (s.length == 0) return 0; 1630 + let hash = 0; 1631 + for (let i = 0; i < s.length; i++) { 1632 + hash = (hash << 5) - hash + s.charCodeAt(i); 1633 + hash |= 0; // Convert to 32bit integer 1634 + } 1635 + return hash; 1636 + } 1637 + 1638 + 1639 + setInterval(() => { 1640 + multipleSplats(5); 1641 + }, 500)
+52
src/lib/cards/PopfeedReviews/PopfeedReviewsCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { CardDefinitionsByType } from '..'; 6 + import Rating from './Rating.svelte'; 7 + 8 + let { item }: { item: Item } = $props(); 9 + 10 + const data = getAdditionalUserData(); 11 + // svelte-ignore state_referenced_locally 12 + let feed = $state(data[item.cardType] as any); 13 + 14 + let did = getDidContext(); 15 + let handle = getHandleContext(); 16 + 17 + onMount(async () => { 18 + console.log(feed); 19 + if (!feed) { 20 + feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 21 + did, 22 + handle 23 + })) as any; 24 + 25 + console.log(feed); 26 + 27 + data[item.cardType] = feed; 28 + } 29 + }); 30 + </script> 31 + 32 + <div class="z-10 flex h-full gap-4 overflow-x-scroll p-4"> 33 + {#each feed ?? [] as review} 34 + <a 35 + rel="noopener noreferrer" 36 + target="_blank" 37 + class="flex" 38 + href="https://popfeed.social/review/{review.uri}" 39 + > 40 + <div 41 + class="relative flex aspect-[2/3] h-full flex-col items-center justify-end overflow-hidden rounded-xl p-1" 42 + > 43 + <img src={review.value.posterUrl} alt="" class="absolute inset-0 -z-10" /> 44 + <div 45 + class="from-base-900/80 absolute right-0 bottom-0 left-0 h-1/3 bg-gradient-to-t via-transparent" 46 + ></div> 47 + 48 + <Rating class="z-10 text-lg" rating={review.value.rating} /> 49 + </div> 50 + </a> 51 + {/each} 52 + </div>
+51
src/lib/cards/PopfeedReviews/Rating.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '@foxui/core'; 3 + 4 + let { rating, class: className }: { rating: number; class?: string } = $props(); 5 + 6 + // Convert 0-10 rating to 0-5 stars 7 + const starRating = $derived(Math.max(0, Math.min(10, rating ?? 0)) / 2); 8 + const fullStars = $derived(Math.floor(starRating)); 9 + const hasHalfStar = $derived(rating % 2 === 1); 10 + const emptyStars = $derived(Math.max(0, 5 - fullStars - (hasHalfStar ? 1 : 0))); 11 + </script> 12 + 13 + {#snippet star(className: string)} 14 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 {className}"> 15 + <path 16 + fill="currentColor" 17 + fill-rule="evenodd" 18 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 19 + clip-rule="evenodd" 20 + /> 21 + </svg> 22 + {/snippet} 23 + 24 + {#snippet halfStar()} 25 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-accent-500"> 26 + <defs> 27 + <linearGradient id="half-filled" x1="0%" y1="0%" x2="100%" y2="0%"> 28 + <stop offset="50%" stop-color="currentColor" /> 29 + <stop offset="50%" stop-color="var(--color-base-400)" /> 30 + </linearGradient> 31 + </defs> 32 + <path 33 + fill="url(#half-filled)" 34 + fill-rule="evenodd" 35 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 36 + clip-rule="evenodd" 37 + /> 38 + </svg> 39 + {/snippet} 40 + 41 + <div class={cn('text-accent-500 flex', className)}> 42 + {#each Array(fullStars) as _} 43 + {@render star("text-accent-500")} 44 + {/each} 45 + {#if hasHalfStar} 46 + {@render halfStar()} 47 + {/if} 48 + {#each Array(emptyStars) as _} 49 + {@render star("text-base-400")} 50 + {/each} 51 + </div>
+21
src/lib/cards/PopfeedReviews/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import { listRecords } from '$lib/oauth/atproto'; 3 + import PopfeedReviewsCard from './PopfeedReviewsCard.svelte'; 4 + 5 + export const PopfeedReviewsCardDefinition = { 6 + type: 'recentPopfeedReviews', 7 + contentComponent: PopfeedReviewsCard, 8 + createNew: (card) => { 9 + card.w = 8; 10 + card.mobileW = 8; 11 + card.h = 3; 12 + card.mobileH = 6; 13 + }, 14 + loadData: async (items, { did }) => { 15 + const data = await listRecords({ did, collection: 'social.popfeed.feed.review' }); 16 + 17 + return data; 18 + }, 19 + minH: 3, 20 + sidebarButtonText: 'Popfeed Reviews' 21 + } as CardDefinition & { type: 'recentPopfeedReviews' };
+10 -1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 25 25 src={profile.avatar} 26 26 class="bg-base-200 dark:bg-base-700 accent:bg-accent-300 aspect-square size-28 rounded-full" 27 27 alt="" 28 + loading="lazy" 28 29 /> 29 - <div class="text-md line-clamp-1 text-center font-bold"> 30 + <div class="text-md line-clamp-1 text-center font-bold max-w-full"> 30 31 {profile.displayName || profile.handle} 31 32 </div> 32 33 </a> 33 34 {/each} 34 35 </div> 35 36 </div> 37 + 38 + 39 + https://coverartarchive.org/release/46656572-9172-44cc-9efa-7ee9b87fe489/front-250 40 + https://coverartarchive.org/release/d8e8f57d-40b6-45e3-8c01-8816d33995d1/front-250 41 + 42 + 42bed944-e781-4447-82df-8cae4918dd94 43 + 44 + d8e8f57d-40b6-45e3-8c01-8816d33995d1
+39
src/lib/cards/TealFMPlaysCard/AlbumArt.svelte
··· 1 + <script lang="ts"> 2 + let { releaseMbId, alt }: { releaseMbId: string; alt: string } = $props(); 3 + 4 + let isLoading = $state(true); 5 + let hasError = $state(false); 6 + </script> 7 + 8 + {#if isLoading} 9 + <div class="bg-base-200 dark:bg-base-800 h-10 w-10 animate-pulse rounded-lg"></div> 10 + {/if} 11 + 12 + {#if hasError} 13 + <div 14 + class="bg-base-300 dark:bg-base-800 accent:bg-accent-700/50 flex h-10 w-10 items-center justify-center rounded-lg" 15 + > 16 + <svg 17 + class="text-base-400 dark:text-base-600 accent:text-accent-900 h-5 w-5" 18 + fill="currentColor" 19 + viewBox="0 0 20 20" 20 + > 21 + <path 22 + d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" 23 + /> 24 + </svg> 25 + </div> 26 + {:else} 27 + <img 28 + src="https://coverartarchive.org/release/{releaseMbId}/front-250" 29 + {alt} 30 + class="h-10 w-10 rounded-lg object-cover {isLoading && 'hidden'}" 31 + onload={() => { 32 + isLoading = false; 33 + }} 34 + onerror={() => { 35 + isLoading = false; 36 + hasError = true; 37 + }} 38 + /> 39 + {/if}
+86
src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { CardDefinitionsByType } from '..'; 6 + import AlbumArt from './AlbumArt.svelte'; 7 + import { RelativeTime } from '@foxui/time'; 8 + 9 + let { item }: { item: Item } = $props(); 10 + 11 + const data = getAdditionalUserData(); 12 + // svelte-ignore state_referenced_locally 13 + let feed = $state(data[item.cardType] as any); 14 + 15 + let did = getDidContext(); 16 + let handle = getHandleContext(); 17 + 18 + onMount(async () => { 19 + console.log(feed); 20 + if (!feed) { 21 + feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 22 + did, 23 + handle 24 + })) as any; 25 + 26 + console.log(feed); 27 + 28 + data[item.cardType] = feed; 29 + } 30 + }); 31 + 32 + function isNumeric(str: string) { 33 + if (typeof str != 'string') return false; // we only process strings! 34 + return ( 35 + !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... 36 + !isNaN(parseFloat(str)) 37 + ); // ...and ensure strings of whitespace fail 38 + } 39 + </script> 40 + 41 + {#snippet musicItem(play)} 42 + <div class="flex w-full items-center gap-3"> 43 + <div class="size-10 shrink-0"> 44 + <AlbumArt releaseMbId={play.value.releaseMbId} alt="" /> 45 + </div> 46 + <div class="min-w-0 flex-1"> 47 + <div class="inline-flex w-full max-w-full justify-between gap-2"> 48 + <div 49 + class="text-accent-500 accent:text-accent-950 min-w-0 flex-1 shrink truncate font-semibold" 50 + > 51 + {play.value.trackName} 52 + </div> 53 + 54 + {#if play.value.playedTime} 55 + <div class="shrink-0 text-xs"> 56 + <RelativeTime 57 + date={new Date( 58 + isNumeric(play.value.playedTime) 59 + ? parseInt(play.value.playedTime) * 1000 60 + : play.value.playedTime 61 + )} 62 + locale="en-US" 63 + /> ago 64 + </div> 65 + {:else} 66 + <div></div> 67 + {/if} 68 + </div> 69 + <div class="my-1 min-w-0 gap-2 truncate text-xs whitespace-nowrap"> 70 + {(play?.value?.artists ?? []).map((a) => a.artistName).join(', ')} 71 + </div> 72 + </div> 73 + </div> 74 + {/snippet} 75 + 76 + <div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4"> 77 + {#each (feed ?? []).slice(0, 20) as play} 78 + {#if play.value.originUrl} 79 + <a href={play.value.originUrl} target="_blank" rel="noopener noreferrer" class="w-full"> 80 + {@render musicItem(play)} 81 + </a> 82 + {:else} 83 + {@render musicItem(play)} 84 + {/if} 85 + {/each} 86 + </div>
+25
src/lib/cards/TealFMPlaysCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import { listRecords } from '$lib/oauth/atproto'; 3 + import TealFMPlaysCard from './TealFMPlaysCard.svelte'; 4 + 5 + export const TealFMPlaysCardDefinition = { 6 + type: 'recentTealFMPlays', 7 + contentComponent: TealFMPlaysCard, 8 + createNew: (card) => { 9 + card.w = 4; 10 + card.mobileW = 8; 11 + card.h = 3; 12 + card.mobileH = 6; 13 + }, 14 + loadData: async (items, { did }) => { 15 + const data = await listRecords({ 16 + did, 17 + collection: 'fm.teal.alpha.feed.play', 18 + limit: 99 19 + }); 20 + 21 + return data; 22 + }, 23 + minW: 4, 24 + sidebarButtonText: 'teal.fm Plays' 25 + } as CardDefinition & { type: 'recentTealFMPlays' };
+7 -1
src/lib/cards/index.ts
··· 18 18 import { YoutubeCardDefinition } from './YoutubeVideoCard'; 19 19 import { BlueskyProfileCardDefinition } from './BlueskyProfileCard'; 20 20 import { GithubProfileCardDefitition } from './GitHubProfileCard'; 21 + import { FluidTextCardDefinition } from './FluidTextCard'; 22 + import { PopfeedReviewsCardDefinition } from './PopfeedReviews'; 23 + import { TealFMPlaysCardDefinition } from './TealFMPlaysCard'; 21 24 22 25 export const AllCardDefinitions = [ 23 26 ImageCardDefinition, ··· 38 41 DinoGameCardDefinition, 39 42 BlueskyProfileCardDefinition, 40 43 GithubProfileCardDefitition, 41 - TetrisCardDefinition 44 + TetrisCardDefinition, 45 + FluidTextCardDefinition, 46 + PopfeedReviewsCardDefinition, 47 + TealFMPlaysCardDefinition 42 48 ] as const; 43 49 44 50 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+1 -1
src/lib/website/Settings.svelte
··· 50 50 (value) => { 51 51 data.publication ??= {}; 52 52 data.publication.preferences ??= {}; 53 - data.publication.preferences.hideProfile = value; 53 + data.publication.preferences.hideProfileSection = value; 54 54 55 55 data = { ...data }; 56 56 }