mirror of
				https://github.com/bspeice/speice.io
				synced 2025-10-30 17:10:31 -04:00 
			
		
		
		
	Auto-sizing canvas, starting cleanup for display on mobile browsers
This commit is contained in:
		| @ -1,16 +1,21 @@ | ||||
| import Canvas, {PainterContext} from "../src/Canvas"; | ||||
| import {useContext} from "react"; | ||||
| import {SquareCanvas, PainterContext} from "../src/Canvas"; | ||||
| import {useContext, useEffect} from "react"; | ||||
|  | ||||
| export function Render({f}) { | ||||
|     const {setPainter} = useContext(PainterContext); | ||||
|     setPainter(f); | ||||
|     const {width, height, setPainter} = useContext(PainterContext); | ||||
|     useEffect(() => { | ||||
|         if (width && height) { | ||||
|             const painter = f({width, height}); | ||||
|             setPainter(painter); | ||||
|         } | ||||
|     }, [width, height]); | ||||
|     return <></>; | ||||
| } | ||||
|  | ||||
| export default function Gasket({f}) { | ||||
|     return ( | ||||
|         <Canvas width={500} height={500}> | ||||
|         <SquareCanvas> | ||||
|             <Render f={f}/> | ||||
|         </Canvas> | ||||
|         </SquareCanvas> | ||||
|     ) | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| import {useEffect, useState, useContext} from "react"; | ||||
| import {useEffect, useState, useContext, useRef} from "react"; | ||||
| import {PainterContext} from "../src/Canvas"; | ||||
| import {chaosGameWeighted} from "./chaosGameWeighted"; | ||||
| import TeX from '@matejmazur/react-katex'; | ||||
| @ -30,7 +30,7 @@ export default function GasketWeighted() { | ||||
|     const weightInput = (title, weight, setWeight) => ( | ||||
|         <> | ||||
|             <div className={styles.inputElement}> | ||||
|                 <p><TeX>{title}</TeX> weight: {weight}</p> | ||||
|                 <p><TeX>{title}</TeX>: {weight}</p> | ||||
|                 <input type={'range'} min={0} max={1} step={.01} style={{width: '100%'}} value={weight} | ||||
|                     onInput={e => setWeight(Number(e.currentTarget.value))}/> | ||||
|             </div> | ||||
| @ -39,7 +39,7 @@ export default function GasketWeighted() { | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: 'auto auto auto'}}> | ||||
|             <div className={styles.inputGroup} style={{display: 'grid', gridTemplateColumns: '1fr 1fr 1fr'}}> | ||||
|                 {weightInput("F_0", f0Weight, setF0Weight)} | ||||
|                 {weightInput("F_1", f1Weight, setF1Weight)} | ||||
|                 {weightInput("F_2", f2Weight, setF2Weight)} | ||||
|  | ||||
| @ -1,30 +1,33 @@ | ||||
| // Hint: try increasing the iteration count | ||||
| const iterations = 10000; | ||||
| // Hint: try changing the iteration count | ||||
| const iterations = 100000; | ||||
|  | ||||
| // Hint: negating `x` and `y` creates some interesting images | ||||
| const transforms = [ | ||||
|     (x, y) => [x / 2, y / 2], | ||||
|     (x, y) => [(x + 1) / 2, y / 2], | ||||
|     (x, y) => [x / 2, (y + 1) / 2] | ||||
| // Hint: negating `x` and `y` creates some cool images | ||||
| const xforms = [ | ||||
|   (x, y) => [x / 2, y / 2], | ||||
|   (x, y) => [(x + 1) / 2, y / 2], | ||||
|   (x, y) => [x / 2, (y + 1) / 2] | ||||
| ] | ||||
|  | ||||
| function* chaosGame() { | ||||
|     let image = new ImageData(500, 500); | ||||
|     let [x, y] = [randomBiUnit(), randomBiUnit()]; | ||||
| function* chaosGame({width, height}) { | ||||
|   let img = new ImageData(width, height); | ||||
|   let [x, y] = [ | ||||
|     randomBiUnit(), | ||||
|     randomBiUnit() | ||||
|   ]; | ||||
|  | ||||
|     for (var count = 0; count < iterations; count++) { | ||||
|         const i = randomInteger(0, transforms.length); | ||||
|         [x, y] = transforms[i](x, y); | ||||
|   for (let c = 0; c < iterations; c++) { | ||||
|     const i = randomInteger(0, xforms.length); | ||||
|     [x, y] = xforms[i](x, y); | ||||
|  | ||||
|         if (count > 20) | ||||
|             plot(x, y, image); | ||||
|     if (c > 20) | ||||
|       plot(x, y, img); | ||||
|  | ||||
|         if (count % 1000 === 0) | ||||
|             yield image; | ||||
|     } | ||||
|     if (c % 1000 === 0) | ||||
|       yield img; | ||||
|   } | ||||
|  | ||||
|     yield image; | ||||
|   yield img; | ||||
| } | ||||
|  | ||||
| // Wiring so the code above displays properly | ||||
| render(<Gasket f={chaosGame()}/>) | ||||
| render(<Gasket f={chaosGame}/>) | ||||
|  | ||||
| @ -6,27 +6,33 @@ import {Transform} from "../src/transform"; | ||||
| const iterations = 50_000; | ||||
| const step = 1000; | ||||
| // hidden-end | ||||
| export type ChaosGameWeightedProps = { | ||||
| type Props = { | ||||
|     width: number, | ||||
|     height: number, | ||||
|     transforms: [number, Transform][] | ||||
| } | ||||
| export function* chaosGameWeighted({width, height, transforms}: ChaosGameWeightedProps) { | ||||
|     let image = new ImageData(width, height); | ||||
|     var [x, y] = [randomBiUnit(), randomBiUnit()]; | ||||
| export function* chaosGameWeighted( | ||||
|     {width, height, transforms}: Props | ||||
| ) { | ||||
|   let img = new ImageData(width, height); | ||||
|   let [x, y] = [ | ||||
|       randomBiUnit(), | ||||
|       randomBiUnit() | ||||
|   ]; | ||||
|  | ||||
|     for (let i = 0; i < iterations; i++) { | ||||
|         // highlight-start | ||||
|         const [_, transform] = randomChoice(transforms); | ||||
|         // highlight-end | ||||
|         [x, y] = transform(x, y); | ||||
|   const iterations = width * height * 0.5; | ||||
|   for (let c = 0; c < iterations; c++) { | ||||
|     // highlight-start | ||||
|     const [_, xform] = randomChoice(transforms); | ||||
|     // highlight-end | ||||
|     [x, y] = xform(x, y); | ||||
|  | ||||
|         if (i > 20) | ||||
|             plot(x, y, image); | ||||
|     if (c > 20) | ||||
|       plot(x, y, img); | ||||
|  | ||||
|         if (i % step === 0) | ||||
|             yield image; | ||||
|     } | ||||
|     if (c % step === 0) | ||||
|       yield img; | ||||
|   } | ||||
|  | ||||
|     yield image; | ||||
|   yield img; | ||||
| } | ||||
| @ -101,7 +101,7 @@ export const shiftData = simpleData.map(({x, y}) => { return {x: x + 1, y} }) | ||||
| This is a simple example designed to illustrate the principle. In general, $F_i$ functions have the form: | ||||
|  | ||||
| $$ | ||||
| F_i(x,y) = (a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i) | ||||
| F_i(x,y) = (a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i) | ||||
| $$ | ||||
|  | ||||
| The parameters ($a_i$, $b_i$, etc.) are values we get to choose. In the example above, we can represent our shift | ||||
| @ -126,10 +126,10 @@ Fractal flames use more complex functions to produce a wide variety of images, b | ||||
| Using these definitions, we can build the first image. The paper defines a function system for us: | ||||
|  | ||||
| $$ | ||||
| F_0(x, y) = \left({x \over 2}, {y \over 2} \right) | ||||
| \hspace{0.8cm} | ||||
| F_1(x, y) = \left({{x + 1} \over 2}, {y \over 2} \right) | ||||
| \hspace{0.8cm} | ||||
| F_0(x, y) = \left({x \over 2}, {y \over 2} \right) \\ | ||||
| ~\\ | ||||
| F_1(x, y) = \left({{x + 1} \over 2}, {y \over 2} \right) \\ | ||||
| ~\\ | ||||
| F_2(x, y) = \left({x \over 2}, {{y + 1} \over 2} \right) | ||||
| $$ | ||||
|  | ||||
| @ -141,11 +141,11 @@ Next, how do we find out all the points in $S$? The paper lays out an algorithm | ||||
|  | ||||
| $$ | ||||
| \begin{align*} | ||||
| &(x, y) = \text{a random point in the bi-unit square} \\ | ||||
| &(x, y) = \text{random point in the bi-unit square} \\ | ||||
| &\text{iterate } \{ \\ | ||||
| &\hspace{1cm} i = \text{a random integer from 0 to } n - 1 \text{ inclusive} \\ | ||||
| &\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\ | ||||
| &\hspace{1cm} (x,y) = F_i(x,y) \\ | ||||
| &\hspace{1cm} \text{plot}(x,y) \text{ except during the first 20 iterations} \\ | ||||
| &\hspace{1cm} \text{plot}(x,y) \text{ if iterations} > 20 \\ | ||||
| \} | ||||
| \end{align*} | ||||
| $$ | ||||
| @ -199,6 +199,6 @@ import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted"; | ||||
| <CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock> | ||||
|  | ||||
| import GasketWeighted from "./GasketWeighted"; | ||||
| import Canvas from "../src/Canvas"; | ||||
| import {SquareCanvas} from "../src/Canvas"; | ||||
|  | ||||
| <Canvas><GasketWeighted/></Canvas> | ||||
| <SquareCanvas><GasketWeighted/></SquareCanvas> | ||||
| @ -1,34 +1,52 @@ | ||||
| export function plot(x: number, y: number, image: ImageData) { | ||||
|     // Translate (x,y) coordinates to pixel coordinates; | ||||
|     // also known as a "camera" function. | ||||
|     // | ||||
|     // The display range we care about is x=[0, 1], y=[0, 1], | ||||
|     // so our pixelX and pixelY coordinates are easy to calculate: | ||||
|     const pixelX = Math.floor(x * image.width); | ||||
|     const pixelY = Math.floor(y * image.height); | ||||
|  | ||||
|     // If we have an (x,y) coordinate outside the display range, | ||||
|     // skip it | ||||
|     if ( | ||||
|         pixelX < 0 || | ||||
|         pixelX > image.width || | ||||
|         pixelY < 0 || | ||||
|         pixelY > image.height | ||||
|     ) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // ImageData is an array that contains four bytes per pixel | ||||
|     // (one for each of the red, green, blue, and alpha values). | ||||
|     // The (pixelX, pixelY) coordinates are used to find where | ||||
|     // in the image we need to write. | ||||
|     const index = pixelY * (image.width * 4) + pixelX * 4; | ||||
|  | ||||
|     // Set the pixel to black by writing a 0 to the first three | ||||
|     // bytes (red, green, blue), and 256 to the last byte (alpha), | ||||
|     // starting at our index: | ||||
|     image.data[index] = 0; | ||||
|     image.data[index + 1] = 0; | ||||
|     image.data[index + 2] = 0; | ||||
|     image.data[index + 3] = 0xff; | ||||
| /** | ||||
|  * ImageData is an array that contains | ||||
|  * four elements per pixel (one for each | ||||
|  * red, green, blue, and alpha value). | ||||
|  * This maps from pixel coordinates | ||||
|  * to the array index | ||||
|  */ | ||||
| function imageIndex( | ||||
|   width: number, | ||||
|   x: number, | ||||
|   y: number | ||||
| ) { | ||||
|   return y * (width * 4) + x * 4; | ||||
| } | ||||
|  | ||||
| export function plot( | ||||
|   x: number, | ||||
|   y: number, | ||||
|   img: ImageData | ||||
| ) { | ||||
|   // Translate (x,y) coordinates | ||||
|   // to pixel coordinates. | ||||
|   // Also known as a "camera" function. | ||||
|   // | ||||
|   // The display range is: | ||||
|   //  x=[0, 1] | ||||
|   //  y=[0, 1] | ||||
|   let pixelX = Math.floor(x * img.width); | ||||
|   let pixelY = Math.floor(y * img.height); | ||||
|  | ||||
|   const index = imageIndex( | ||||
|       img.width, | ||||
|       pixelX, | ||||
|       pixelY | ||||
|   ); | ||||
|  | ||||
|   // Skip pixels outside the display range | ||||
|   if ( | ||||
|       index < 0 || | ||||
|       index > img.data.length | ||||
|   ) { | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   // Set the pixel to black by writing 0 | ||||
|   // to the first three elements, | ||||
|   // and 255 to the last element | ||||
|   img.data[index] = 0; | ||||
|   img.data[index + 1] = 0; | ||||
|   img.data[index + 2] = 0; | ||||
|   img.data[index + 3] = 0xff; | ||||
| } | ||||
| @ -145,7 +145,7 @@ each transform. | ||||
| import Canvas from "../src/Canvas"; | ||||
| import FlameBlend from "./FlameBlend"; | ||||
|  | ||||
| <Canvas><FlameBlend/></Canvas> | ||||
| <!-- <Canvas><FlameBlend/></Canvas> --> | ||||
|  | ||||
| ## Post transforms | ||||
|  | ||||
| @ -160,10 +160,10 @@ $$ | ||||
|  | ||||
| import FlamePost from "./FlamePost"; | ||||
|  | ||||
| <Canvas><FlamePost/></Canvas> | ||||
| <!-- <Canvas><FlamePost/></Canvas> --> | ||||
|  | ||||
| ## Final transform | ||||
|  | ||||
| import FlameFinal from "./FlameFinal"; | ||||
|  | ||||
| <Canvas><FlameFinal/></Canvas> | ||||
| <!-- <Canvas><FlameFinal/></Canvas> --> | ||||
| @ -32,7 +32,7 @@ import Canvas from "../src/Canvas"; | ||||
| import FlameHistogram from "./FlameHistogram"; | ||||
| import {paintLinear} from "./paintLinear"; | ||||
|  | ||||
| <Canvas><FlameHistogram quality={5} paint={paintLinear}/></Canvas> | ||||
| <!-- <Canvas><FlameHistogram quality={5} paint={paintLinear}/></Canvas> --> | ||||
|  | ||||
| ## Log display | ||||
|  | ||||
| @ -42,7 +42,7 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic" | ||||
|  | ||||
| import {paintLogarithmic} from './paintLogarithmic' | ||||
|  | ||||
| <Canvas><FlameHistogram quality={5} paint={paintLogarithmic}/></Canvas> | ||||
| <!-- <Canvas><FlameHistogram quality={5} paint={paintLogarithmic}/></Canvas> --> | ||||
|  | ||||
| ## Color | ||||
|  | ||||
| @ -52,4 +52,4 @@ import paintColorSource from "!!raw-loader!./paintColor" | ||||
|  | ||||
| import FlameColor from "./FlameColor"; | ||||
|  | ||||
| <Canvas><FlameColor quality={15}/></Canvas> | ||||
| <!-- <Canvas><FlameColor quality={15}/></Canvas> --> | ||||
| @ -1,138 +1,68 @@ | ||||
| import React, {useCallback, useEffect, useState, createContext, useRef} from "react"; | ||||
| import React, {useEffect, useState, createContext, useRef} from "react"; | ||||
| import {useColorMode} from "@docusaurus/theme-common"; | ||||
| import BrowserOnly from "@docusaurus/BrowserOnly"; | ||||
|  | ||||
| function invertImage(sourceImage: ImageData): ImageData { | ||||
|     const image = new ImageData(sourceImage.width, sourceImage.height); | ||||
|     sourceImage.data.forEach((value, index) => | ||||
|         image.data[index] = index % 4 === 3 ? value : 0xff - value) | ||||
|  | ||||
|     return image; | ||||
| } | ||||
|  | ||||
| type InvertibleCanvasProps = { | ||||
|     width: number, | ||||
|     height: number, | ||||
|     // NOTE: Images are provided as a single-element array | ||||
|     //so we can allow re-painting with the same (modified) ImageData reference. | ||||
|     image?: [ImageData], | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Draw images to a canvas, automatically inverting colors as needed. | ||||
|  * | ||||
|  * @param width Canvas width | ||||
|  * @param height Canvas height | ||||
|  * @param hidden Hide the canvas element | ||||
|  * @param image Image data to draw on the canvas | ||||
|  */ | ||||
| export const InvertibleCanvas: React.FC<InvertibleCanvasProps> = ({width, height, image}) => { | ||||
|     const [canvasCtx, setCanvasCtx] = useState<CanvasRenderingContext2D>(null); | ||||
|     const canvasRef = useCallback(node => { | ||||
|         if (node !== null) { | ||||
|             setCanvasCtx(node.getContext("2d")); | ||||
|         } | ||||
|     }, []); | ||||
|  | ||||
|     const [paintImage, setPaintImage] = useState<[ImageData]>(null); | ||||
|     useEffect(() => { | ||||
|         if (canvasCtx && paintImage) { | ||||
|             canvasCtx.putImageData(paintImage[0], 0, 0); | ||||
|         } | ||||
|     }, [canvasCtx, paintImage]); | ||||
|  | ||||
|     const {colorMode} = useColorMode(); | ||||
|     useEffect(() => { | ||||
|         if (image) { | ||||
|             setPaintImage(colorMode === 'light' ? image : [invertImage(image[0])]); | ||||
|         } | ||||
|     }, [image, colorMode]); | ||||
|  | ||||
|     return ( | ||||
|         <canvas | ||||
|             ref={canvasRef} | ||||
|             width={width} | ||||
|             height={height} | ||||
|             style={{aspectRatio: width / height}} | ||||
|         /> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| type PainterProps = { | ||||
|     width: number; | ||||
|     height: number; | ||||
|     setPainter: (painter: Iterator<ImageData>) => void; | ||||
| } | ||||
| export const PainterContext = createContext<PainterProps>(null); | ||||
| export const PainterContext = createContext<PainterProps>(null) | ||||
|  | ||||
| interface CanvasProps { | ||||
|     width?: number; | ||||
|     height?: number; | ||||
|     children?: React.ReactElement; | ||||
| type CanvasProps = { | ||||
|     style?: any; | ||||
|     children?: React.ReactElement | ||||
| } | ||||
| export const Canvas: React.FC<CanvasProps> = ({style, children}) => { | ||||
|     const canvasRef = useRef<HTMLCanvasElement>(null); | ||||
|  | ||||
| /** | ||||
|  * Draw fractal flames to a canvas. | ||||
|  * | ||||
|  * This component is a bit involved because it attempts to solve | ||||
|  * a couple problems at once: | ||||
|  *  - Incrementally drawing an image to the canvas | ||||
|  *  - Interrupting drawing with new parameters | ||||
|  * | ||||
|  * Running a full render is labor-intensive, so we model it | ||||
|  * as an iterator that yields an image of the current system. | ||||
|  * Internally, that iterator is re-queued on each new image; | ||||
|  * so long as retrieving each image happens quickly, | ||||
|  * we keep the main loop running even with CPU-heavy code. | ||||
|  * As a side benefit, this also animates the chaos game nicely. | ||||
|  * TODO(bspeice): This also causes React to complain about maximum update depth | ||||
|  * Would this be better off spawning a `useEffect` animator | ||||
|  * that has access to a `setState` queue? | ||||
|  * | ||||
|  * To interrupt drawing, children set the active iterator | ||||
|  * through the context provider. This component doesn't care | ||||
|  * about which iterator is in progress, it exists only | ||||
|  * to fetch the next image and paint it to our canvas. | ||||
|  * | ||||
|  * TODO(bspeice): Can we make this "re-queueing iterator" pattern generic? | ||||
|  * It would be nice to have iterators returning arbitrary objects, | ||||
|  * but we rely on contexts to manage the iterator, and I can't find | ||||
|  * a good way to make those generic. | ||||
|  */ | ||||
| export default function Canvas({width, height, children}: CanvasProps) { | ||||
|     const viewportDetectionRef = useRef<HTMLDivElement>(null); | ||||
|     const [isVisible, setIsVisible] = useState(false); | ||||
|     useEffect(() => { | ||||
|         if (!viewportDetectionRef) { | ||||
|         if (!canvasRef.current) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const observer = new IntersectionObserver(([entry]) => { | ||||
|         const observer = new IntersectionObserver((entries) => { | ||||
|             const [entry] = entries; | ||||
|             if (entry.isIntersecting) { | ||||
|                 setIsVisible(true); | ||||
|             } | ||||
|         }, {root: null, threshold: .1}); | ||||
|         observer.observe(viewportDetectionRef.current); | ||||
|         }); | ||||
|         observer.observe(canvasRef.current); | ||||
|  | ||||
|         return () => { | ||||
|             if (viewportDetectionRef.current) { | ||||
|                 observer.unobserve(viewportDetectionRef.current); | ||||
|             if (canvasRef.current) { | ||||
|                 observer.unobserve(canvasRef.current); | ||||
|             } | ||||
|         } | ||||
|     }, [viewportDetectionRef]); | ||||
|     }, [canvasRef]); | ||||
|  | ||||
|     const [width, setWidth] = useState(0); | ||||
|     const [height, setHeight] = useState(0); | ||||
|     useEffect(() => { | ||||
|         if (canvasRef.current) { | ||||
|             setWidth(canvasRef.current.offsetWidth); | ||||
|             setHeight(canvasRef.current.offsetHeight); | ||||
|         } | ||||
|     }, [canvasRef]); | ||||
|  | ||||
|     const [imageHolder, setImageHolder] = useState<[ImageData]>(null); | ||||
|     useEffect(() => { | ||||
|         if (canvasRef.current && imageHolder) { | ||||
|             canvasRef.current.getContext("2d").putImageData(imageHolder[0], 0, 0); | ||||
|         } | ||||
|     }, [canvasRef, imageHolder]); | ||||
|  | ||||
|     const [image, setImage] = useState<[ImageData]>(null); | ||||
|     const [painterHolder, setPainterHolder] = useState<[Iterator<ImageData>]>(null); | ||||
|     useEffect(() => { | ||||
|         if (!isVisible || !painterHolder) { | ||||
|             console.log("Skipping, not visible"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const painter = painterHolder[0]; | ||||
|         const nextImage = painter.next().value; | ||||
|         if (nextImage) { | ||||
|             setImage([nextImage]); | ||||
|             setImageHolder([nextImage]); | ||||
|             setPainterHolder([painter]); | ||||
|         } else { | ||||
|             setPainterHolder(null); | ||||
| @ -146,18 +76,25 @@ export default function Canvas({width, height, children}: CanvasProps) { | ||||
|         } | ||||
|     }, [painter]); | ||||
|  | ||||
|     width = width ?? 500; | ||||
|     height = height ?? 500; | ||||
|     const {colorMode} = useColorMode(); | ||||
|     return ( | ||||
|         <> | ||||
|             <center> | ||||
|                 <div ref={viewportDetectionRef}> | ||||
|                     <InvertibleCanvas width={width} height={height} image={image}/> | ||||
|                 </div> | ||||
|             </center> | ||||
|             <canvas | ||||
|                 ref={canvasRef} | ||||
|                 width={width} | ||||
|                 height={height} | ||||
|                 style={{ | ||||
|                     filter: colorMode === 'dark' ? 'invert(1)' : '', | ||||
|                     ...style | ||||
|                 }} | ||||
|             /> | ||||
|             <PainterContext.Provider value={{width, height, setPainter}}> | ||||
|                 <BrowserOnly>{() => children}</BrowserOnly> | ||||
|             </PainterContext.Provider> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export const SquareCanvas: React.FC<CanvasProps> = ({style, children}) => { | ||||
|     return <Canvas style={{width: '100%', aspectRatio: '1/1', ...style}} children={children}/> | ||||
| } | ||||
| @ -1,6 +1,7 @@ | ||||
| .inputGroup { | ||||
|     padding: .5em; | ||||
|     margin: .5em; | ||||
|     padding: .2em; | ||||
|     margin-top: .2em; | ||||
|     margin-bottom: .2em; | ||||
|     border: 1px solid; | ||||
|     border-radius: var(--ifm-global-radius); | ||||
|     border-color: var(--ifm-color-emphasis-500); | ||||
|  | ||||
| @ -1,15 +1,20 @@ | ||||
| export function randomChoice<T>(choices: [number, T][]): [number, T] { | ||||
|     const weightSum = choices.reduce((sum, [weight, _]) => sum + weight, 0); | ||||
|     let choice = Math.random() * weightSum; | ||||
| export function randomChoice<T>( | ||||
|   choices: [number, T][] | ||||
| ): [number, T] { | ||||
|   const weightSum = choices.reduce( | ||||
|       (sum, [weight, _]) => sum + weight, | ||||
|       0 | ||||
|   ); | ||||
|   let choice = Math.random() * weightSum; | ||||
|  | ||||
|     for (const [index, element] of choices.entries()) { | ||||
|         const [weight, t] = element; | ||||
|         if (choice < weight) { | ||||
|             return [index, t]; | ||||
|         } | ||||
|         choice -= weight; | ||||
|   for (const [idx, elem] of choices.entries()) { | ||||
|     const [weight, t] = elem; | ||||
|     if (choice < weight) { | ||||
|       return [idx, t]; | ||||
|     } | ||||
|     choice -= weight; | ||||
|   } | ||||
|  | ||||
|     const index = choices.length - 1; | ||||
|     return [index, choices[index][1]]; | ||||
|   const index = choices.length - 1; | ||||
|   return [index, choices[index][1]]; | ||||
| } | ||||
| @ -1,3 +1,7 @@ | ||||
| export function randomInteger(min: number, max: number) { | ||||
|     return Math.floor(Math.random() * (max - min)) + min; | ||||
| export function randomInteger( | ||||
|     min: number, | ||||
|     max: number | ||||
| ) { | ||||
|     let v = Math.random() * (max - min); | ||||
|     return Math.floor(v) + min; | ||||
| } | ||||
| @ -3,6 +3,19 @@ | ||||
|     --ifm-container-width-xl: 1440px; | ||||
|     --ifm-footer-padding-vertical: .5rem; | ||||
|     --ifm-spacing-horizontal: .8rem; | ||||
|  | ||||
|     /* Reduce padding on code blocks */ | ||||
|     --ifm-pre-padding: .6rem; | ||||
|  | ||||
|     /* More readable code highlight background */ | ||||
|     --docusaurus-highlighted-code-line-bg: var(--ifm-color-emphasis-300); | ||||
|  | ||||
|     /*--ifm-code-font-size: 85%;*/ | ||||
| } | ||||
|  | ||||
| .katex { | ||||
|     /* Default is 1.21, this helps with fitting on mobile screens */ | ||||
|     font-size: 1.16em; | ||||
| } | ||||
|  | ||||
| .header-github-link:hover { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user