1/// <reference lib="dom" /> 2 3import { data } from "./bandit-data.js?v=03cb5ee5"; 4 5function randomInt(min, max) { 6 return Math.floor(Math.random() * (max - min)) + min; 7} 8 9const eventInit = { 10 bubbles: true, 11 composed: true, 12 cancelable: true, 13}; 14 15export class BanditEvent extends Event { 16 static eventName = "bandit"; 17 #type = ""; 18 /** @type {"ready"|"start"|"stop"} */ 19 get type() { 20 return this.#type; 21 } 22 /** 23 * @param {"ready"|"start"|"stop"} type 24 */ 25 constructor(type) { 26 super(BanditEvent.eventName, eventInit); 27 this.#type = type; 28 } 29} 30 31const sevenResolvers = Promise.withResolvers(); 32const seven = new Image(300, 300); 33seven.src = `/assets/images/bandit-seven.svg`; 34seven.onload = sevenResolvers.resolve; 35seven.onerror = (err) => console.error(err); 36 37const { devicePixelRatio } = globalThis; 38 39const ids = data.map(({ id }) => id); 40 41const lowRes = ids.map((id) => { 42 const image = new Image(300, 300); 43 image.src = `/assets/images/bandit-${id}.avif`; 44 return image; 45}); 46 47const highRes = ids.map((id) => { 48 const image = new Image(300, 300); 49 image.src = `/assets/images/bandit-${id}.svg`; 50 return image; 51}); 52 53const ready = Promise.all( 54 [ 55 sevenResolvers.promise, 56 [...lowRes, ...highRes].map((image) => { 57 const { promise, resolve } = Promise.withResolvers(); 58 image.onload = resolve; 59 image.onerror = (err) => console.error(err); 60 return promise; 61 }), 62 ], 63); 64 65export class Component extends HTMLElement { 66 static tag = "bandit-spinner"; 67 68 static { 69 globalThis.customElements.define(this.tag, Component); 70 } 71 72 /** @type {HTMLCanvasElement} */ 73 #canvas; 74 #size = 100; 75 #spinning = false; 76 #index = -1; 77 78 /** @type {CanvasRenderingContext2D} */ 79 get #ctx() { 80 return this.#canvas.getContext("2d"); 81 } 82 83 get imageIndex() { 84 return this.#index; 85 } 86 87 get isSpinning() { 88 return this.#spinning; 89 } 90 91 constructor() { 92 super(); 93 this.#canvas = document.createElement("canvas"); 94 } 95 96 connectedCallback() { 97 ready.then(() => this.#setup()); 98 } 99 100 #setup() { 101 this.querySelector("img").replaceWith(this.#canvas); 102 this.dispatchEvent(new BanditEvent("ready")); 103 const observer = new ResizeObserver((entries) => { 104 for (const entry of entries) { 105 if (entry.target === this) { 106 this.#reset(entry.contentRect.width); 107 } 108 } 109 }); 110 observer.observe(this); 111 } 112 113 #reset(size = 100) { 114 let image = seven; 115 if (this.imageIndex !== -1) { 116 image = highRes[this.imageIndex]; 117 } 118 this.#size = size; 119 this.#canvas.width = this.#size * devicePixelRatio; 120 this.#canvas.height = this.#size * devicePixelRatio; 121 this.#ctx.scale(devicePixelRatio, devicePixelRatio); 122 this.#ctx.drawImage(image, 0, 0, this.#size, this.#size); 123 } 124 125 stop() { 126 this.#spinning = false; 127 } 128 129 spin() { 130 if (this.#spinning) return; 131 this.#index = -1; 132 this.#spinning = true; 133 134 const indexes = new Set(); 135 while (indexes.size < 3) indexes.add(randomInt(0, lowRes.length)); 136 const index = Array.from(indexes); 137 this.#index = index[0]; 138 139 const fps = 1000 / 60; 140 let last = 0; 141 const frames = 5; 142 let frameIndex = 0; 143 144 const size = this.#size; 145 146 this.#ctx.clearRect(0, 0, size, size); 147 148 const frame = (time) => { 149 if (time - last >= fps) { 150 last = time - ((time - last) % fps); 151 const y = ((frameIndex + 1) / frames) * size; 152 this.#ctx.drawImage(lowRes[index[0]], 0, y - size, size, size); 153 this.#ctx.drawImage(lowRes[index[1]], 0, y, size, size); 154 this.#ctx.drawImage(lowRes[index[2]], 0, y + size, size, size); 155 frameIndex++; 156 if (frameIndex >= frames) { 157 if (this.#spinning === false) { 158 this.#ctx.clearRect(0, 0, size, size); 159 this.#ctx.drawImage(highRes[index[0]], 0, 0, size, size); 160 this.dispatchEvent(new BanditEvent("stop")); 161 return; 162 } 163 frameIndex = 0; 164 index[2] = index[1]; 165 index[1] = index[0]; 166 while (index[0] === index[1] || index[0] === index[2]) { 167 index[0] = randomInt(0, lowRes.length); 168 } 169 this.#index = index[0]; 170 } 171 } 172 requestAnimationFrame(frame); 173 }; 174 requestAnimationFrame(frame); 175 176 this.dispatchEvent(new BanditEvent("start")); 177 } 178}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.