ãã²ããã¶ãã§ãããã®èšäºã¯ã¯ãœã¢ã㪠Advent Calendar 2021ã®13æ¥ç®ã§ã
çªç¶ã§ããçããã¯ãã¹ã¯ããŒã«ã«åãããŠãµãã£ãšåºãŠããwebããŒãžãã£ãŠã©ãæããŸãïŒãæè¿å€ãã§ããããããã
確ãã«ãªã·ã£ã¬ã ã楜ãããã²ãŒã ãšãäœå®¶ããã®ã®ã£ã©ãªãŒãµã€ããšããªãå
šç¶OKãã§ãæ
å ±ã欲ãããŠã¢ã¯ã»ã¹ããŠãããŒãžã§ããµãã£ãã£ãŠããããšã¡ãã£ãšã€ã©ããšããã
ã¡ãã£ãšïŒ...ã€ã©ããšïŒ...ããã èš±ããªãã絶察
ããããããªãã°ç²ç ã
ããæ§ã®ãµã€ããç²ç ããéœåäžãä»åã®å®è£ ã¯Chromeæ©èœæ¡åŒµã§ããæ©èœæ¡åŒµã¯Viteã«Chromeæ©èœæ¡åŒµçšã®ãã©ã°ã€ã³vite-plugin-chrome-extensionãå ¥ããŠäœããŸãããä»åã¯è§£èª¬ããªããã©ãããè¶ æ¥œããã¬ãŒã ã¯ãŒã¯ã¯ç¡ããèšèªã¯TypeScriptã§ãã
ãšã¯èšããã¯ãœã¢ããªã®ããã«Chromeã«æ©èœæ¡åŒµãã€ã³ã¹ããŒã«ããŠãããé çãªæ¹ã¯å°ãªããšæãã®ã§ãã€ã³ã¹ããŒã«äžèŠã®ãã¢ããŒãžãäœããŸãããããŒãžéããŠã¹ã¯ããŒã«ãããšèªåã§ã²ãŒã ãå§ãŸããŸãã
- ããŒãžå ã§æ€åºãããã¢ãã¡ãŒã·ã§ã³ã«èµ€æ ãã€ããŸã
- ã¢ãã¡ãŒã·ã§ã³äžã«ã¯ãªãã¯ããã°ç²ç ã§ããŸã
- ç²ç ãããšå€§ããã«å¿ããŠãªããšãªããã€ã³ããã€ããŸãã
- ã¯ãªã¢ãã²ãŒã ãªãŒããŒããããŸãããã ã£ãŠã¯ãœã¢ããªã ãã®
- ããäžåºŠãã¬ã€ãããæã¯ãäžçªäžãŸã§ã¹ã¯ããŒã«ãæ»ããŠãããªããŒãããŠãã ãããïŒã ã£ãŠks(ç¥ïŒ
å®ç©ã®webãµã€ãã§åäœãããæ§åãèŒããŠãããŸãããããã©ãã®ãµã€ãã§ãã£ãŠãã©ãèããŠãã«ããç«ã€ã®ã§åŒç€Ÿæ¡çšãµã€ãã§ãããŸãããããã¯ããã§æãããããªæ°ããããã©ããåŒç€Ÿåªä»ãwebã奜ããªç±ããšã³ãžãã¢ç©æ¥µåéäžã§ãïŒïŒãã£ãŠæžããŠããã°èš±ããŠãããããããããªããæ°ã«ãªãæ¹ã¯DMãã ããã
以äžãã¡ãã£ãšæè¡çãªã話ã§ã
å šãŠã®ã¢ãã¡ãŒã·ã§ã³ãæ€åºããã
å šãŠã®ã¢ãã¡ãŒã·ã§ã³ãç²ç ããã«ã¯ããŸãå šãŠã®ã¢ãã¡ãŒã·ã§ã³ãæ€åºããªããšãããŸãããèªåã®äœã£ãã³ã³ãã³ããªãç°¡åã ãã©ãããæ§ã®ãµã€ãã¯ã©ããªãã¬ãŒã ã¯ãŒã¯ãã©ã€ãã©ãªã䜿ã£ãŠããããããããªãã®ã§ãã¡ãã£ãšå·¥å€«ãå¿ èŠã§ãã
1. CSS Transition / CSS Animationãæ€åºãã
webããŒãžãã¢ãã¡ãŒã·ã§ã³ãããäžçªæ軜ãªæ¹æ³ãCSS TransitionãšCSS Animationã§ããã«ãŒãœã«ã®ãããšã¡ãã£ãšåããããã¹ã¯ããŒã«ã§ãµãã£ãšåºãŠããããããªããããäžçªæ¥œã
ã§ãããã€ããæ€åºããæ¹æ³ã§ãããCSS TransitionãšCSS Animationã«ã¯ããããtransitionstart
ãšanimationstart
ã£ãŠã€ãã³ããããã®ã§ãããæŸããŸãããã®ã€ãã³ããmousedown
ã¿ãããªããããã€ãã³ãåæ§ã«ãããªã³ã°ã§ã€ãã³ããäŒæããŸããããã«ãããããããæ¡ãã€ã¶ãããšãæ®éã¯ããªãã¯ããã€ãŸããâã®ããã«body
ã§ç¶²ã匵ã£ãŠããã°å
šãŠè£è¶³ã§ããŸãã
const onstart = (ev) => {
console.log('transition started!', ev.target)
// èµ€æ ãã€ãã
ev.target.style.outline = '2px solid red'
}
document.body.addEventListener('transitionstart', onstart)
document.body.addEventListener('animationstart', onstart)
åæ§ã«ã¢ãã¡ãŒã·ã§ã³ã®çµäºã¯transitionend
ãšanimationend
ã€ãã³ãã§æŸããŸãããã£ãŠã
-
transitionstart
/animationstart
ã§èŠçŽ ãç²ç 察象ã«è¿œå -
transitionend
/animationend
ã§èŠçŽ ãç²ç 察象ããåé€
ãšããã°ãåããŠãããã®ã ããç²ç ã®å¯Ÿè±¡ã«ã§ãããã§ãããã£ããïŒ
...ã£ãŠæããããïŒ
2. Transition / Animationã䜿ããã«åãããŠãããã€ããæ€åºãã
å®éäžã®ã³ãŒãã§èµ€æ ãã€ããŠã¿ããšãããã®ã§ããããµã€ãã«ãã£ãŠã¯ãã®æ¹æ³ã§ã¯æ€åºã§ããªãã¢ãã¡ãŒã·ã§ã³ãçµæ§ãããŸããäžçªããããã®ããsetInterval
ãrequestAnimationFrame
ã䜿ã£ãŠãããã¿ãŒã³ã
const el = document.getElementById('neko')
const move = () => {
const sec = Date.now() / 1000
const y = Math.sin(sec) * 100
el.style.transform = `translateY(${y}px)`
requestAnimationFrame(move)
}
move()
åºæ¬çã«ã¯CSS Transition / CSS Animationã䜿ã£ãæ¹ãããã©ãŒãã³ã¹ã¯è¯ãã®ã ãã©ãè€éãªåããæ¹ãå¶åŸ¡ãå¿
èŠãªã±ãŒã¹ã§ã¯setInterval
/ requestAnimationFrame
æ¹åŒãçµæ§äœ¿ãããŸããGSAPã¿ãããªã¢ãã¡ãŒã·ã§ã³ã©ã€ãã©ãªã倧æµã¯ãã®ãã¿ãŒã³ã®ã¯ãã
ã§ããã®ãã¿ãŒã³ã¯çµå±ããŸãã«ã¹ã¿ã€ã«ãæžãæããŠããã ããªã®ã§ãtransitionstart
/ animationstart
ã¿ãããªã€ãã³ããçºç«ããªãããã§ããã®ãŸãŸã§ã¯æ€åºãã§ããŸããã
ããã§ç»å Žããã®ãå°ã£ãæã®MutationObserverã§ãããã®åãIEã§ãããµããŒãããŠãæšæºæ©èœã®ããã«æ» å€ã«ç»å Žããããšã®ãªã圱ã®åãªã®ã ãã©ãããããã±ãŒã¹ã§ã¯æäžäž»ã«ãªãã®ã§èŠããŠãããŠæã¯ãªãã§ãã
const observer = new MutationObserver(function (mutations) {
const els = mutations.forEach((rec) => {
console.log('styleãå€ãããŸãã', rec.target)
})
})
observer.observe(document.body, {
subtree: true, // åå«èŠçŽ å
šãŠãç£èŠãã
attributes: true, // ç£èŠå¯Ÿè±¡ = å±æ§
attributeFilter: ['style'], // ç£èŠããã®ã¯styleã®ã¿
})
ãšã¯ãããããã§æŸããã®ã¯ãèŠçŽ ã®style
ãå€ãã£ããããšã ããªã®ã§ãããããããã«ãã£ã«ã¿ããŠãtransform
ãwidth
ãšãã£ãã¢ãã¡ãŒã·ã§ã³ç³»ã®ããããã£ãå€ãã£ããã©ãããããã§ãã¯ããŸãã
-
transform
ãwidth
ãšãã£ãã¢ãã¡ãŒã·ã§ã³ç³»ã®ããããã£ãå€ãã£ãããããèšé² - äžå®æéå ã«å床ããããã£ãå€ãã£ãããã¢ãã¡ãŒã·ã§ã³éå§ããšèŠåããŠç²ç 察象ã«è¿œå
- äžå®æéå€åããªããã°ãã¢ãã¡ãŒã·ã§ã³çµäºããšèŠåããŠç²ç 察象ããåé€
æŽçãããšãããªæµãã§ããé¢åã ãã©ããããšã¯ã·ã³ãã«ã§ãã
å®éã®ãœãŒã¹ã§ã¯ã1. CSS Transition / CSS Animationãæ¹åŒãšã2. ã¿ã€ã㌠/ requestAnimationFrameãæ¹åŒã®2ã€ã®çµæãããŒãžããŠã1ã€ã®ã€ã³ã¿ãã§ãŒã¹ã§ç²ç 察象ã®è¿œå /åé€ãåãåããããã«æŽããŠããŸãã
watchAllAnimatiedElements((added, removed, all) => {
added.map((element) => {
console.log('ã¢ãã¡ãŒã·ã§ã³éå§', element.el)
})
removed.map((element) => {
console.log('ã¢ãã¡ãŒã·ã§ã³çµäº', element.el)
})
})
ããã§ç²ç ãã¹ãèŠçŽ ã®ãªã¹ãã¯æã«å
¥ããŸããã次ã¯ãããå®éã«ç²ç ããŸãã
ãªããã¬ã¢ãã£ã©ãšããŠä»¥äžã®ãããªã¢ãã¡ãŒã·ã§ã³ãããã®ã§ãããä»åã¯èŠãªããµãããŠãŸãã
- SVGã®
<animate>
èŠçŽ ã䜿ã£ãã¢ãã¡ãŒã·ã§ã³
â ãããé 匵ãã°ã€ãã³ããæŸããã¯ããä»åã¯é¢åãªã®ã§ãã¹ - Canvasã䜿ã£ãã¢ãã¡ãŒã·ã§ã³
â ããã¯ã ãªãããããšãããšå®éã®æç»å 容ãç»åãšããŠè§£æããªããšãããªããªãã¯ã
å šãŠã®ã¢ãã¡ãŒã·ã§ã³ãã¯ãªãã¯ã§ç²ç ããã
现ããããšã¯çœ®ããŠãããŠãæµã®ãªã¹ããæã«å ¥ã£ãã®ã§ãããã¯ãªãã¯ã§ç²ç ããä»çµã¿ãäœããŸãã
1. click
ã€ãã³ãã§ç²ç
ããã¯è¶ ç°¡åãã¢ãã¡ãŒã·ã§ã³ãå§ãŸã£ããã€ãã³ããã³ãã©ãã»ããããŠçµãã£ããåé€ããã ãã
// ã¯ãªãã¯ããæã®ã€ãã³ããã³ãã©
const onDirectClick = (ev) => {
ev.preventDefault()
ev.stopImmediatePropagation()
ev.target.removeEventListener('click', onDirectClick)
element.el.style.outline = '2px solid blue'
}
// ç²ç 察象ã®è¿œå ã»åé€ãç£èŠ
watchAllAnimatiedElements((added, removed, all) => {
added.map((element) => {
console.log('ã¢ãã¡ãŒã·ã§ã³éå§', element.el)
element.el.style.outline = '2px solid red'
element.el.addEventListener('click', onDirectClick)
})
removed.map((element) => {
console.log('ã¢ãã¡ãŒã·ã§ã³çµäº', element.el)
element.el.style.outline = ''
element.el.removeEventListener('click', onDirectClick)
})
})
ã€ãã³ããã³ãã©ã§ã¯æ ã®è²ãå€ããŠããã以äžã€ãã³ããäŒæããªãããã«stopImmediatePropagation()
ãåŒã³ãŸããããšãèŠçŽ ã<a>
ã¿ã°ã ã£ãããããšç»é¢é·ç§»ããŠããŸãã®ã§ãç²ç æã«ã¯ãªã³ã¯ãåããªãããpreventDefault()
ã§ãããã¯ããŠãããŸãã
...ãããããã§ããããšæã£ããã æåã¯ã
2. elementsFromPoint()
ã§ã¯ãªãã¯åº§æšã®èŠçŽ ãç²ç
å®éã«ã¯ãããã§æŸããªãã±ãŒã¹ãã¡ããã¡ãããããããã¡ãªã®ãäžã«å¥ãªèŠçŽ ãä¹ã£ã¡ãã£ãŠããã¿ãŒã³ãæåã«æåããã£ãŠãèæ¯ã ããµãã£ãšåºãŠãããããªãã¶ã€ã³ã§ããã
ããããªããšãæŸãããããŸãããŠãbody
èŠçŽ ã«ç¶²ã匵ããŸãã
// ã¢ãã¡ãŒã·ã§ã³äžã®èŠçŽ = ç²ç 察象ã®ãªã¹ã
let movingElems = []
// ç²ç 察象ã®è¿œå ã»åé€ãç£èŠ
watchAllAnimatiedElements((added, removed, all) => {
movingElems = [...all]
// ç¥
})
// bodyã§ç¶²ã匵ã
document.body.addEventListener('pointerdown', (ev) => {
// ã¯ãªãã¯äœçœ®ã®äžã«ããå
šãŠã®èŠçŽ ãååŸ
const elsAtPoint = elementsFromPoint(ev.clientX, ev.clientY)
// ç²ç 察象ã®å
šèŠçŽ
const elsMoving = movingElems.map((element) => element.el)
// ç²ç 察象 & ã¯ãªãã¯äœçœ®ã®äžã«ããèŠçŽ ãæœåº
const els = elsAtPoint.filter((el) => elsMoving.includes(el))
// èŠã€ãã£ããã®ãç²ç
els.forEach((el) => el.style.outline = '2px solid blue')
})
ããã ãã¿ããšç°¡åã§ãããelementsFromPoint()
ã§ã¯ãªãã¯åº§æšã«ããå
šãŠã®èŠçŽ ãååŸãããã®äžã«ç²ç 察象ãããã°ãŸãšããŠå
šéšç²ç ããã ãã§ãã
åé¡ã¯ãelement s FromPoint()ããªããŠäŸ¿å©ãªé¢æ°ãååšããªãããšã§ããã¯ãªãã¯äœçœ®ã®äžçªäžã«ããèŠçŽ ãååŸããelementFromPoint()ïŒelementsãããªããŠelementïŒã¯æšæºã§ååšãããã©ãã¯ãªãã¯äœçœ®ã®å šãŠã®èŠçŽ ãååŸããé¢æ°ã¯ãªãã®ã§ãã
OKãç¡ããªãäœããããªãã
ã§ãã§ããã®ãããâãäœãã£ãŠèšã£ããã©stackoverflowã§èŠã€ããåçã®çŒãçŽãã§ãã
export const elementsFromPoint = (
x: number,
y: number
): (HTMLElement | SVGElement)[] => {
const rollbackList: {
el: HTMLElement | SVGElement
pointerEvents: string
}[] = []
const results: (HTMLElement | SVGElement)[] = []
let current: Element | null = document.elementFromPoint(x, y)
while (current) {
if (current.tagName.toLowerCase() === 'html') break
if (!(current instanceof HTMLElement || current instanceof SVGElement)) {
break
}
results.push(current)
rollbackList.push({
el: current,
pointerEvents: current.style.pointerEvents,
})
current.style.pointerEvents = 'none'
current = document.elementFromPoint(x, y)
}
rollbackList.forEach(
(ent) => (ent.el.style.pointerEvents = ent.pointerEvents)
)
return results
}
èªãã§ãããã人ã¯ããŸããã...ãã£ãŠæã£ãããããªããã©ãããããããŸããã...ãã£ãŠæã£ãã
èªãæ°ã®ãªã人åãã«ãã£ãã説æãããšãæšæºã®elementFromPoint()
ïŒã¯ãªãã¯äœçœ®ã®äžçªäžã«ããèŠçŽ ãååŸããïŒã䜿ã£ãŠã
- ã¯ãªãã¯åº§æšã®äžçªäžã®èŠçŽ ãååŸ
- èŠã€ãã£ãèŠçŽ ã®
style.pointerEvents
ãnone
ã«å€ããŠã¯ãªãã¯ã«åå¿ããªãããã«ãã - 1-2ãç¹°ãè¿ã
- äžçªäžã®
<html>
èŠçŽ ã«ã¶ã€ãã£ããçµäº - æžãæãã
style.pointerEvents
ãå ã®å€ã«æ»ã - èŠã€ãã£ãå šèŠçŽ ãè¿ã
ã£ãŠæµãããããããã¿ãã¿ãªã³ãŒãæžããã®ã¯10幎ã¶ãããããªæ°ããã...
ãšããããããã§å€§æµã®ãã¿ãŒã³ã§ç²ç 察象ã®ã¯ãªãã¯ãã§ããããã«ãªããŸããã
ã¯ãªãã¯ãããèŠãç®ãã¡ãããšç²ç ããã
ãããŸã§ã§æŠãç®çã¯éããŸãããæºè¶³
æåŸã¯ããŸãã§ãã¯ãªãã¯æã«ããã£ãœãç²ç ããæŒåºãè¿œå ããŸãã
Element.animate()
ã§ãšãã§ã¯ããè¿œå ãã
ãããŸã§ã®äŸã§ã¯åã«el.style.outline = '2px solid blue'
ã§éæ ãæããã ãã ã£ããã©ãå®éã«ã¯âãããªæãã®ã³ãŒãã«ãªã£ãŠãŸãïŒ
export const showHitEffect = (el) => {
el.animate(
[
{
opacity: el.style.opacity,
transform: 'scale(0.8)',
filter: `blur(0) url('#${NOISE_FILTER_ID}')`,
},
{
opacity: 0,
transform: 'scale(1.5)',
filter: `blur(10px) url('#${NOISE_FILTER_ID}')`,
},
],
{
fill: 'forwards',
duration: 500,
}
)
}
animate()
ã¯CSS Animationãšåæ§ä»¥äžã®ããšãã§ãããããšæ°ãç®ã®API(Web Animations API)ã§ããäž»èŠãã©ãŠã¶ã§ã¯ã»ãŒäœ¿ããŸããããã䜿ã£ãŠopacity
ãfilter
ã調æŽããŠç²ç ããæŒåºãäœããŸãã
åçŽã«CSSã®ã¯ã©ã¹åãè¿œå ããŠæŒåºå
容ã¯CSSåŽã§å®çŸ©ãã圢ã§ãåãã¯ããŸãããïŒå
ã®ïŒwebããŒãžåŽã§el.className = 'hoge'
ã®ããã«ã¯ã©ã¹ãæžãæããããŠããŸããšäžçºã§æŒåºã解é€ãããŠããŸããããããŸãè¯ãæ¹æ³ã§ã¯ãããŸãããä»å䜿ã£ãanimate()
ã«ããã¹ã¿ã€ã«ã®å€æŽã¯CSSåŽã®æå®ãããåžžã«åŒ·ãã®ã§çµ¶å¯Ÿç²ç ãã匷ãæå¿ã瀺ãã®ã«ã¯æé©ã§ãã
SVGãã£ã«ã¿ã§ç²ç æãäžãã
transform
ãfilter: blur()
ã ãã§ã¯ã€ãã€ãç²ç æã足ããªãã®ã§ãä»åã¯ä»äžãã«SVGãã£ã«ã¿ãŒãåœãŠãŸããäžã®animate()
ã§filter
ããããã£ã«èšå®ããŠãããã®ã§ããã
èªäœã®ãã£ãŒã«ã¿ãŒã¯âãããªæããã¡ãã£ãšéã ãã©ã<feTurbulence>
ãš<feDisplacementMap>
ã§å
¥åã€ã¡ãŒãžã«ããã¬ã©ã¹ã£ãœããã€ãºãåœãŠãSVGãã£ã«ã¿ãäœã£ãŠbody
çŽäžã«è¿œå ããŠãããŸãã
// filterãå®çŸ©ããSVGèŠçŽ ãäœã£ãŠããã¥ã¡ã³ãã«è¿œå ãã
export const initSvgFilter = () => {
const NS = 'http://www.w3.org/2000/svg'
const el = document.createElementNS(NS, 'svg')
el.innerHTML = `
<filter id='${NOISE_FILTER_ID}' x='0%' y='0%' width='110%' height='110%'>
<feTurbulence type="turbulence" baseFrequency="0.2 0.2" result="NOISE" numOctaves="2" />
<feDisplacementMap in="SourceGraphic" in2="NOISE" scale="16" xChannelSelector="R" yChannelSelector="R" />
</filter>`
document.body.appendChild(el)
}
æåã«å®çŸ©ããŠããã°ãããšã¯Element.animate()
ãCSSå®çŸ©ããfilter: url('ID')
ã®åœ¢ã§å®çŸ©ãããã£ã«ã¿ãé©çšã§ããŸãããã£ã«ã¿ã®æå®ã¯æ®å¿µãªããIDå±æ§ã䜿ãä»ãªãã®ã§ããã®æã®æ©èœæ¡åŒµã§äœ¿ãå Žåã«ã¯å
ããŒãžã®ã³ã³ãã³ããšéè€ããªããããé·ãã®ååãã€ããæ¹ãè¯ãã§ãã
ããŸãïŒ åãã®å€ããµã€ãã¯prefers-reduced-motion
ã䜿ãã
äœè«ã ãã©ãããããŒããäœããããµã€ããã§ããã°ãå®ã¯ç²ç ãããšãæŒåºçšã®ã¢ãã¡ãŒã·ã§ã³ãæ¢ããããšãã§ããŸããMacãiPhoneã§ããã°ãèŠå·®å¹æãæžãããããªã³ã«ããããšã§ãWindowsã®å Žåã¯ãWindowsã«ã¢ãã¡ãŒã·ã§ã³ã衚瀺ãããããªãã«ããããšã§ããããŸãåãããªãã§ïŒãã£ãŠå®£èšããããšãã§ããŸãã
ïŒWindowsã¯è©ŠããŠãªãã®ã§éã£ãŠããæããŠãã ããïŒ
ãããèšå®ãããŠãããã©ããã¯JSãCSSã®ã¡ãã£ã¢ã¯ãšãªã§æŸããã®ã§ãwebãµã€ããäœããšãã¯ããããèŠãŠå¿ é ã§ã¯ãªãã¢ãã¡ãŒã·ã§ã³ãåã£ãŠããŸãã®ã芪åã§ããâã¿ããã«äžåŸOFFã«ãã¡ããïŒæ£ç¢ºã«ã¯æ¥µããŠçæéã§çµããã¢ãã¡ãŒã·ã§ã³ã«ããŠããŸãïŒã®ããããåé ã§åºããåŒç€Ÿãµã€ããã ããããããªæãã«ãªã£ãŠãŸãã
/* ãèŠå·®å¹æãæžãããããªã³ãªãã¢ãã¡ãŒã·ã§ã³ããªã */
@media (prefers-reduced-motion: reduce) {
animation-delay: 0s !important;
animation-duration: 1ms !important;
transition-delay: 0s !important;
transition-duration: 1ms !important;
}
ã¢ãã¡ãŒã·ã§ã³ã¯çšæ³å®¹éãå®ã£ãŠäœ¿ãã
ãããªããšç²ç ãããïŒ