Mercator Filters

Apply filters & transforms to your Google Meet webcam

目前為 2020-07-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Mercator Filters
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Apply filters & transforms to your Google Meet webcam
// @author       Xing
// @match        https://meet.google.com/*
// @grant        none
// ==/UserScript==

(async function() {
	'use strict'

	// Create form

	const form = document.createElement('form')

	const style = document.createElement('style')
	style.innerText = `
form, form * {
	box-sizing: border-box
}

form {
	position: fixed;
	left: 0;
	top: 0;
	width: 480px;
	max-width: 100vw;
	height: 110vh;
	z-index: 9999999;
	background: #fffa;
	backdrop-filter: blur(1rem);
	padding: 1rem;
	transition: transform 200ms;
	transform: translateY(-100vh);
	box-shadow: 0 0 4rem #0004;
	border-bottom-right-radius: 10vh;
}

form:hover {
	transform: none;
}

#previews {
	position: absolute;
	cursor: pointer;
	width: 400px;
	height: calc(10vh - 1rem);
	bottom: 1rem;
	display: flex;
}

#previews>* {
	height: 100%;
	width: auto;
	background: magenta;
	transform: scaleX(-1);
}

#previews>:first-child {
	margin-right: 1rem;
}

form:hover>#previews {
	bottom: calc(10vh + 1rem);
	height: fit-content;
}

form:hover>#previews>* {
	width: 50%;
	height: auto;
}

label {
	display: flex;
	justify-content: space-between;
	align-items: center;
}

input {
	width: 80%;
	-webkit-appearance: none;
	--gradient: transparent, transparent;
	--rainbow: hsl(0, 80%, 75%), hsl(30, 80%, 75%), hsl(60, 80%, 75%), hsl(90, 80%, 75%), hsl(120, 80%, 75%), hsl(150, 80%, 75%), hsl(180, 80%, 75%), hsl(210, 80%, 75%), hsl(240, 80%, 75%), hsl(270, 80%, 75%), hsl(300, 80%, 75%), hsl(330, 80%, 75%);
	background: linear-gradient(90deg, var(--gradient)), linear-gradient(90deg, var(--rainbow));
	border-radius: 100px;
	border: 4px solid gray;
}

input::-webkit-slider-thumb {
	-webkit-appearance: none;
	background: white;
	width: 16px;
	height: 16px;
	border: 4px solid black;
	border-radius: 8px
}

#exposure,
#fog,
#vignette {
	--gradient: black, #8880, white
}

#contrast {
	--gradient: gray, #8880
}

#temperature {
	--gradient: #4af, #8880, #fa4
}

#tint {
	--gradient: #f8f, #8880, #8f8
}

#sepia {
	--gradient: #8880, #aa8
}

#hue, #rotate {
	background: linear-gradient(90deg, hsl(0, 80%, 75%), hsl(60, 80%, 75%), hsl(120, 80%, 75%), hsl(180, 80%, 75%), hsl(240, 80%, 75%), hsl(300, 80%, 75%), hsl(0, 80%, 75%), hsl(60, 80%, 75%), hsl(120, 80%, 75%), hsl(180, 80%, 75%), hsl(240, 80%, 75%), hsl(300, 80%, 75%), hsl(0, 80%, 75%))
}

#saturate {
	--gradient: gray, #8880 50%, blue, magenta
}

#blur {
	--gradient: #8880, gray
}

#scale, #x, #y,
#pillarbox, #letterbox
{
	--gradient: black, white
}
`
	form.appendChild(style)

	// Create sliders

	const sliders = Object.fromEntries(
		'exposure,contrast,temperature,tint,sepia,hue,saturate,blur,fog,vignette,rotate,scale,x,y,pillarbox,letterbox'
		.split(',')
		.map( key => {

			let slider = document.createElement('input')

			slider.type = 'range'

			slider.min = [
				'blur',
				'sepia',
				'scale',
				'pillarbox',
				'letterbox'
			].includes(key) ? 0 : -1

			slider.max = 1
			slider.step = 0.01
			slider.value = 0

			let label = document.createElement('label')

			label.innerText = slider.title = slider.id = key

			form.appendChild(label)
			label.appendChild(slider)

			return [key,slider]
		})
	)

	const reset = document.createElement('button')
	reset.innerText = 'reset'
	reset.addEventListener('click',event=>{
		event.preventDefault()
		Object.values(sliders).forEach(slider=>{
			slider.value = 0
		})
	})
	form.appendChild(reset)

	// Create color balance matrix

	const filter = document.createElementNS('http://www.w3.org/2000/svg','filter')
	filter.id = 'mercator-filters-svg-filter'
	const filter_matrix = document.createElementNS('http://www.w3.org/2000/svg','feColorMatrix')
	filter_matrix.setAttribute('in','SourceGraphic')

	const previews = document.createElement('div')
	previews.id = 'previews'
	form.appendChild(previews)

	// Create preview video

	const video = document.createElement('video')
	video.setAttribute('playsinline','')
	video.setAttribute('autoplay','')
	video.setAttribute('muted','')
	previews.appendChild(video)

	// Create canvas

	const canvas = document.createElement('canvas')
	previews.appendChild(canvas)

	// Add UI to page

	filter.appendChild(filter_matrix)
	form.appendChild(filter)
	document.body.appendChild(form)

	class mercator_filters_MediaStream extends MediaStream {
		constructor(old_stream) {

			// Copy original stream settings

			super(old_stream)

			const constraints = {audio: false, video: true}

			video.srcObject = old_stream

			const old_stream_settings = old_stream.getVideoTracks()[0].getSettings()

			const w = old_stream_settings.width
			const h = old_stream_settings.height
			canvas.width = w
			canvas.height = h
			const canvas_ctx = canvas.getContext('2d')

			// Amp: for values that can range from 0 to +infinity, amp**value does the mapping.

			const amp = 8

			let time = video.currentTime

			function draw(){

				if (time != video.currentTime) {

					// Reset canvas

					canvas_ctx.setTransform(1,0,0,1,0,0)
					canvas_ctx.clearRect(0,0,w,h)

					canvas_ctx.translate(w/2,h/2)

					// Reset values

					sliders.hue.value %= 1
					sliders.rotate.value %= 1


					// BALANCE

					let temperature = sliders.temperature.value
					let warm = Math.max(0, temperature)
					let cool = Math.max(0,-temperature)
					let tint = sliders.tint.value
					filter_matrix.setAttribute('values',`
${1-cool-tint/2} 0 0 0 0
0 ${1-warm/2} 0 0 0
0 0 ${1-warm-tint/2} 0 0
0 0 0 1 0
`)

					// CSS filters

					canvas_ctx.filter = `
brightness(${amp**sliders.exposure.value})
contrast(${amp**sliders.contrast.value})
url('#mercator-filters-svg-filter')
url('#mercator-filters-svg-filter')
sepia(${sliders.sepia.value*100}%)
hue-rotate(${360*sliders.hue.value}deg)
saturate(${amp**sliders.saturate.value*100}%)
blur(${sliders.blur.value*w/32}px)
`


					// Linear transformations: rotation, scaling, translation

					let rotate = sliders.rotate.value
					if (rotate){

						canvas_ctx.rotate(-rotate*2*Math.PI)

					}

					let scale = amp**sliders.scale.value
					if (scale) {

						canvas_ctx.scale(scale,scale)

					}

					canvas_ctx.translate(-sliders.x.value*w,sliders.y.value*h)

					// Apply CSS filters & linear transformations

					canvas_ctx.translate(-w/2,-h/2)

					canvas_ctx.drawImage(video,0,0,w,h)

					// Fog: cover the entire image with a single color

					let fog = sliders.fog.value
					if (fog) {

						let fog_lum = Math.sign(fog)*100
						let fog_alpha = Math.abs(fog)

						canvas_ctx.fillStyle = `hsla(0,0%,${fog_lum}%,${fog_alpha})`
						canvas_ctx.fillRect(0,0,w,h)

					}

					// Vignette: cover the edges of the image with a single color

					let vignette = sliders.vignette.value
					if (vignette) {

						let vignette_lum = Math.sign(vignette)*100
						let vignette_alpha = Math.abs(vignette)
						let vignette_gradient = canvas_ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, ((w/2)**2+(h/2)**2)**(1/2))

						vignette_gradient.addColorStop(0, `hsla(0,0%,${vignette_lum}%,0`)
						vignette_gradient.addColorStop(1, `hsla(0,0%,${vignette_lum}%,${vignette_alpha}`)

						canvas_ctx.fillStyle = vignette_gradient
						canvas_ctx.fillRect(0,0,w,h)

					}

					// Cropping

					let pillarbox = sliders.pillarbox.value*w/2
					if (pillarbox) {

						canvas_ctx.clearRect(0,0,pillarbox,h)
						canvas_ctx.clearRect(w,0,-pillarbox,h)

					}

					let letterbox = sliders.letterbox.value*h/2
					if (letterbox) {

						canvas_ctx.clearRect(0,0,w,letterbox)
						canvas_ctx.clearRect(0,h,w,-letterbox)

					}

				}

				// Recursive call

				requestAnimationFrame(draw)

			}

			draw()

			return canvas.captureStream(30)

		}
	}

	async function mercator_filters_newGetUserMedia(constraints) {
		if (constraints && constraints.video && !constraints.audio ) {
			return new mercator_filters_MediaStream(await navigator.mediaDevices.mercator_filters_oldGetUserMedia(constraints))
		} else {
			return navigator.mediaDevices.mercator_filters_oldGetUserMedia(constraints)
		}
	}

	MediaDevices.prototype.mercator_filters_oldGetUserMedia = MediaDevices.prototype.getUserMedia
	MediaDevices.prototype.getUserMedia = mercator_filters_newGetUserMedia

})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址