作ってみた
まぁ実物は3Dでもっときれいなんだけど、2Dで頑張ってみた
クリックでギュイーンなります
See the Pen Siriみたいなボタン by serna37 (@serna37) on CodePen.
ソース紹介
htmlとcssはほぼ何もやってないので、jsだけ紹介します
長いので、部分ごとに分けて紹介
描画のための設定
- 非同期でスリープ処理
スリープ.js
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
- アニメーション描画用関数(setIntervalより軽い)
アニメーション描画.js
function setAnimationFrame(func, interval) {
let elapsed = 0,
time = Date.now(),
quit = false,
stop = false
const update = () => {
let delta = Date.now() - time
time = Date.now()
elapsed += delta
if (elapsed >= interval) {
elapsed -= ~~(elapsed)
if (!stop) func()
}
if (!quit) requestAnimationFrame(update)
}
//処理中にエラーが発生した場合、強制終了
try {
update()
} catch (e) {
quit = true
console.error(e)
console.trace()
}
return {
// 終了
quit: () => quit = true,
// 一時停止
stop: () => stop = true,
// 再スタート
restart: () => stop = false
}
}
各図形のプロパティを設定
- 下地、原点、色
下地、原点、色.js
const siri = document.createElement('canvas')
siri.width = $('html').width()
siri.height = $('html').height()
$('body').append(siri)
const origin = { x: siri.width / 2, y: siri.height / 1.1 }
// RGBグラデーション変化
function gradRGB(cR, cG, cB, cRMin, cGMin, cBMin, dcR, dcG, dcB) {
// 変化中
if (!(cRMin === cR && cGMin === cG && cBMin === cB)) return { dcR: dcR, dcG: dcG, dcB: dcB }
// 赤終了→緑開始
if (dcG === 0 && dcB === 0) return { dcR: 0, dcG: dcR, dcB: dcB }
// 緑終了→青開始
if (dcR === 0 && dcB === 0) return { dcR: dcR, dcG: 0, dcB: dcG }
// 青終了→赤開始
if (dcR === 0 && dcG === 0) return { dcR: dcB, dcG: dcG, dcB: 0 }
}
- 円
円.js
const pCircle1 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
r: 120, dr: 0.4, rMax: 150, rMin: 120,
updR: () => {
let me = pCircle1
if ((me.rMax <= me.r && me.dr > 0) || (me.rMin >= me.r && me.dr < 0)) me.dr *= -1
me.r += me.dr
},
// 色
cR: 200, dcR: 3, cRMax: 200, cRMin: 100,
cG: 100, dcG: 0, cGMax: 200, cGMin: 100,
cB: 100, dcB: 0, cBMax: 200, cBMin: 100,
updC: () => {
let me = pCircle1
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pCircle1
let asis = me.rMax
me.rMax *= ratio
me.dr *= speed
if (me.dr < 0) me.dr *= -1
}
}
- 楕円
楕円.js
const pEllipse1 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
rx: 40, ry: 90,
drx: 0.1, dry: 0.2,
rxMax: 65, rxMin: 35,
ryMax: 110, ryMin: 80,
updR: () => {
let me = pEllipse1
if ((me.rxMax <= me.rx && me.drx > 0) || (me.rxMin >= me.rx && me.drx < 0)) me.drx *= -1
if ((me.ryMax <= me.ry && me.dry > 0) || (me.ryMin >= me.ry && me.dry < 0)) me.dry *= -1
me.rx += me.drx
me.ry += me.dry
},
// 回転角
rad: -Math.PI / 36, drad: -Math.PI / 36,
updRad: () => {
let me = pEllipse1
me.rad += me.drad
},
// 色
cR: 200, dcR: 3, cRMax: 200, cRMin: 50,
cG: 50, dcG: 0, cGMax: 200, cGMin: 50,
cB: 50, dcB: 0, cBMax: 200, cBMin: 50,
updC: () => {
let me = pEllipse1
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pEllipse1
me.rxMax *= ratio
me.ryMax *= ratio
me.drx *= speed
me.dry *= speed
me.drad *= speed
if (me.drx < 0) me.drx *= -1
if (me.dry < 0) me.dry *= -1
}
}
描画を実行する
- 実行
描く.js
const ctx = siri.getContext('2d')
const circles = Array.of(pCircle1, pCircle2)
const ellipses = Array.of(pEllipse1, pEllipse2, pEllipse3, pEllipse4)
const figures = Array.of(pCircle1, pCircle2, pEllipse1, pEllipse2, pEllipse3, pEllipse4)
// グラデーション
function gradient(ctx, x0, y0, r, cr, cg, cb) {
ctx.globalCompositeOperation = 'lighter'
let g = ctx.createRadialGradient(x0, y0, 0, x0, y0, r)
g.addColorStop(0.0, `rgb(${cr}, ${cg}, ${cb}, 1)`)
g.addColorStop(0.5, `rgb(${cr}, ${cg}, ${cb}, 0.6)`)
g.addColorStop(1.0, `rgb(${cr}, ${cg}, ${cb}, 0.2)`)
ctx.fillStyle = g
}
// 円描画
function dCircle(prof) {
ctx.beginPath()
ctx.arc(prof.x0, prof.y0, prof.r, 0, 2 * Math.PI)
gradient(ctx, prof.x0, prof.y0, prof.r, prof.cR, prof.cG, prof.cB)
ctx.fill()
ctx.closePath()
prof.updR()
prof.updC()
}
// 楕円描画
function dEllipse(prof) {
ctx.beginPath()
ctx.ellipse(prof.x0, prof.y0, prof.rx, prof.ry, prof.rad, 0, 2 * Math.PI)
gradient(ctx, prof.x0, prof.y0, prof.rx, prof.cR, prof.cG, prof.cB)
ctx.fill()
ctx.closePath()
prof.updR()
prof.updRad()
prof.updC()
}
// フレーム単位描画
function drawFrame() {
ctx.clearRect(0, 0, siri.width, siri.height)
circles.forEach(dCircle)
ellipses.forEach(dEllipse)
}
// 実際の描画
const { quit, stop, restart } = setAnimationFrame(drawFrame, 10)
当たり判定
- クリック
タッチイベントを設定.js
// クリック↔原点のユークリッド距離と半径で、当たり判定
function isHit(e) {
let rect = e.target.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top,
distance = Math.sqrt(Math.pow(origin.x - x, 2) + Math.pow(origin.y - y, 2))
return distance < pCircle1.r
}
// 拡大
function expand(ratio, speed) {
figures.forEach(v => v.exp(ratio, speed))
return () => figures.forEach(v => v.exp(1 / ratio, 1 / speed))
}
// ボタン押下時
siri.onclick = async e => {
if (!isHit(e)) return
let undo = expand(1.3, 4)
reveal()
await sleep(1000)
undo()
}
js全量
.js
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
function setAnimationFrame(func, interval) {
let elapsed = 0,
time = Date.now(),
quit = false,
stop = false
const update = () => {
let delta = Date.now() - time
time = Date.now()
elapsed += delta
if (elapsed >= interval) {
elapsed -= ~~(elapsed)
if (!stop) func()
}
if (!quit) requestAnimationFrame(update)
}
//処理中にエラーが発生した場合、強制終了
try {
update()
} catch (e) {
quit = true
console.error(e)
console.trace()
}
return {
// 終了
quit: () => quit = true,
// 一時停止
stop: () => stop = true,
// 再スタート
restart: () => stop = false
}
}
// ========================================
// 図形の設定
// ========================================
const siri = document.createElement('canvas')
siri.width = $('body').width()
siri.height = $('body').height()
$('body').append(siri)
const origin = { x: siri.width / 2, y: siri.height / 2 }
// RGBグラデーション変化
function gradRGB(cR, cG, cB, cRMin, cGMin, cBMin, dcR, dcG, dcB) {
// 変化中
if (!(cRMin === cR && cGMin === cG && cBMin === cB)) return { dcR: dcR, dcG: dcG, dcB: dcB }
// 赤終了→緑開始
if (dcG === 0 && dcB === 0) return { dcR: 0, dcG: dcR, dcB: dcB }
// 緑終了→青開始
if (dcR === 0 && dcB === 0) return { dcR: dcR, dcG: 0, dcB: dcG }
// 青終了→赤開始
if (dcR === 0 && dcG === 0) return { dcR: dcB, dcG: dcG, dcB: 0 }
}
// 円1
const pCircle1 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
r: 120, dr: 0.4, rMax: 150, rMin: 120,
updR: () => {
let me = pCircle1
if ((me.rMax <= me.r && me.dr > 0) || (me.rMin >= me.r && me.dr < 0)) me.dr *= -1
me.r += me.dr
},
// 色
cR: 200, dcR: 3, cRMax: 200, cRMin: 100,
cG: 100, dcG: 0, cGMax: 200, cGMin: 100,
cB: 100, dcB: 0, cBMax: 200, cBMin: 100,
updC: () => {
let me = pCircle1
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pCircle1
let asis = me.rMax
me.rMax *= ratio
me.dr *= speed
if (me.dr < 0) me.dr *= -1
}
}
// 円2
const pCircle2 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
r: 100, dr: 0.4, rMax: 130, rMin: 100,
updR: () => {
let me = pCircle2
if ((me.rMax <= me.r && me.dr > 0) || (me.rMin >= me.r && me.dr < 0)) me.dr *= -1
me.r += me.dr
},
// 色
cR: 200, dcR: 3, cRMax: 200, cRMin: 50,
cG: 50, dcG: 0, cGMax: 200, cGMin: 50,
cB: 50, dcB: 0, cBMax: 200, cBMin: 50,
updC: () => {
let me = pCircle2
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pCircle2
let asis = me.rMax
me.rMax *= ratio
me.dr *= speed
if (me.dr < 0) me.dr *= -1
}
}
// 楕円1
const pEllipse1 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
rx: 40, ry: 90,
drx: 0.1, dry: 0.2,
rxMax: 65, rxMin: 35,
ryMax: 110, ryMin: 80,
updR: () => {
let me = pEllipse1
if ((me.rxMax <= me.rx && me.drx > 0) || (me.rxMin >= me.rx && me.drx < 0)) me.drx *= -1
if ((me.ryMax <= me.ry && me.dry > 0) || (me.ryMin >= me.ry && me.dry < 0)) me.dry *= -1
me.rx += me.drx
me.ry += me.dry
},
// 回転角
rad: -Math.PI / 36, drad: -Math.PI / 36,
updRad: () => {
let me = pEllipse1
me.rad += me.drad
},
// 色
cR: 200, dcR: 3, cRMax: 200, cRMin: 50,
cG: 50, dcG: 0, cGMax: 200, cGMin: 50,
cB: 50, dcB: 0, cBMax: 200, cBMin: 50,
updC: () => {
let me = pEllipse1
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pEllipse1
me.rxMax *= ratio
me.ryMax *= ratio
me.drx *= speed
me.dry *= speed
me.drad *= speed
if (me.drx < 0) me.drx *= -1
if (me.dry < 0) me.dry *= -1
}
}
// 楕円2
const pEllipse2 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
rx: 40, ry: 80,
drx: 0.2, dry: 0.3,
rxMax: 60, rxMin: 30,
ryMax: 100, ryMin: 70,
updR: () => {
let me = pEllipse2
if ((me.rxMax <= me.rx && me.drx > 0) || (me.rxMin >= me.rx && me.drx < 0)) me.drx *= -1
if ((me.ryMax <= me.ry && me.dry > 0) || (me.ryMin >= me.ry && me.dry < 0)) me.dry *= -1
me.rx += me.drx
me.ry += me.dry
},
// 回転角
rad: Math.PI / 72, drad: Math.PI / 72,
updRad: () => {
let me = pEllipse2
me.rad += me.drad
},
// 色
cR: 50, dcR: 3, cRMax: 200, cRMin: 50,
cG: 200, dcG: 0, cGMax: 200, cGMin: 50,
cB: 50, dcB: 0, cBMax: 200, cBMin: 50,
updC: () => {
let me = pEllipse2
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pEllipse2
me.rxMax *= ratio
me.ryMax *= ratio
me.drx *= speed
me.dry *= speed
me.drad *= speed
if (me.drx < 0) me.drx *= -1
if (me.dry < 0) me.dry *= -1
}
}
// 楕円3
const pEllipse3 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
rx: 60, ry: 80,
drx: 0.2, dry: 0.3,
rxMax: 70, rxMin: 40,
ryMax: 110, ryMin: 70,
updR: () => {
let me = pEllipse3
if ((me.rxMax <= me.rx && me.drx > 0) || (me.rxMin >= me.rx && me.drx < 0)) me.drx *= -1
if ((me.ryMax <= me.ry && me.dry > 0) || (me.ryMin >= me.ry && me.dry < 0)) me.dry *= -1
me.rx += me.drx
me.ry += me.dry
},
// 回転角
rad: -Math.PI / 72, drad: -Math.PI / 72,
updRad: () => {
let me = pEllipse3
me.rad += me.drad
},
// 色
cR: 50, dcR: 3, cRMax: 200, cRMin: 50,
cG: 50, dcG: 0, cGMax: 200, cGMin: 50,
cB: 200, dcB: 0, cBMax: 200, cBMin: 50,
updC: () => {
let me = pEllipse3
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pEllipse3
me.rxMax *= ratio
me.ryMax *= ratio
me.drx *= speed
me.dry *= speed
me.drad *= speed
if (me.drx < 0) me.drx *= -1
if (me.dry < 0) me.dry *= -1
}
}
// 楕円4
const pEllipse4 = {
// 原点
x0: origin.x, y0: origin.y,
// 半径
rx: 50, ry: 100,
drx: 0.1, dry: 0.2,
rxMax: 70, rxMin: 30,
ryMax: 120, ryMin: 90,
updR: () => {
let me = pEllipse4
if ((me.rxMax <= me.rx && me.drx > 0) || (me.rxMin >= me.rx && me.drx < 0)) me.drx *= -1
if ((me.ryMax <= me.ry && me.dry > 0) || (me.ryMin >= me.ry && me.dry < 0)) me.dry *= -1
me.rx += me.drx
me.ry += me.dry
},
// 回転角
rad: Math.PI / 144, drad: Math.PI / 144,
updRad: () => {
let me = pEllipse4
me.rad += me.drad
},
// 色
cR: 50, dcR: 3, cRMax: 200, cRMin: 50,
cG: 50, dcG: 0, cGMax: 200, cGMin: 50,
cB: 200, dcB: 0, cBMax: 200, cBMin: 50,
updC: () => {
let me = pEllipse4
if (me.cRMax <= me.cR || me.cRMin >= me.cR) me.dcR *= -1
if (me.cGMax <= me.cG || me.cGMin >= me.cG) me.dcG *= -1
if (me.cBMax <= me.cB || me.cBMin >= me.cB) me.dcB *= -1
let { dcR, dcG, dcB } = gradRGB(me.cR, me.cB, me.cG,
me.cRMin, me.cGMin, me.cBMin, me.dcR, me.dcG, me.dcB)
me.dcR = dcR
me.dcG = dcG
me.dcB = dcB
me.cR += me.dcR
me.cG += me.dcG
me.cB += me.dcB
},
exp: (ratio, speed) => {
let me = pEllipse4
me.rxMax *= ratio
me.ryMax *= ratio
me.drx *= speed
me.dry *= speed
me.drad *= speed
if (me.drx < 0) me.drx *= -1
if (me.dry < 0) me.dry *= -1
}
}
// ====================
// siri設定
// ====================
// def
const ctx = siri.getContext('2d')
const circles = Array.of(pCircle1, pCircle2)
const ellipses = Array.of(pEllipse1, pEllipse2, pEllipse3, pEllipse4)
const figures = Array.of(pCircle1, pCircle2, pEllipse1, pEllipse2, pEllipse3, pEllipse4)
// グラデーション
function gradient(ctx, x0, y0, r, cr, cg, cb) {
ctx.globalCompositeOperation = 'lighter'
let g = ctx.createRadialGradient(x0, y0, 0, x0, y0, r)
g.addColorStop(0.0, `rgb(${cr}, ${cg}, ${cb}, 1)`)
g.addColorStop(0.5, `rgb(${cr}, ${cg}, ${cb}, 0.6)`)
g.addColorStop(1.0, `rgb(${cr}, ${cg}, ${cb}, 0.2)`)
ctx.fillStyle = g
}
// 円描画
function dCircle(prof) {
ctx.beginPath()
ctx.arc(prof.x0, prof.y0, prof.r, 0, 2 * Math.PI)
gradient(ctx, prof.x0, prof.y0, prof.r, prof.cR, prof.cG, prof.cB)
ctx.fill()
ctx.closePath()
prof.updR()
prof.updC()
}
// 楕円描画
function dEllipse(prof) {
ctx.beginPath()
ctx.ellipse(prof.x0, prof.y0, prof.rx, prof.ry, prof.rad, 0, 2 * Math.PI)
gradient(ctx, prof.x0, prof.y0, prof.rx, prof.cR, prof.cG, prof.cB)
ctx.fill()
ctx.closePath()
prof.updR()
prof.updRad()
prof.updC()
}
// フレーム単位描画
function drawFrame() {
ctx.clearRect(0, 0, siri.width, siri.height)
circles.forEach(dCircle)
ellipses.forEach(dEllipse)
}
// ====================
// siri押下イベント
// ====================
// クリック↔原点のユークリッド距離と半径で、当たり判定
function isHit(e) {
let rect = e.target.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top,
distance = Math.sqrt(Math.pow(origin.x - x, 2) + Math.pow(origin.y - y, 2))
return distance < pCircle1.r
}
// 拡大
function expand(ratio, speed) {
figures.forEach(v => v.exp(ratio, speed))
return () => figures.forEach(v => v.exp(1 / ratio, 1 / speed))
}
// ボタン押下時
siri.onclick = async e => {
if (!isHit(e)) return
let undo = expand(1.3, 4)
console.log('open')
await sleep(1000)
undo()
}
// ====================
// 初期表示
// ====================
async function initiate() {
let inicr1 = circles[0].r, inicr2 = circles[1].r
circles.forEach(v => v.r = 0)
ellipses.forEach(v => {
v.rx = 0
v.ry = 0
})
let undo = expand(1, 12)
await sleep(200)
undo()
undo = expand(1, 10)
await sleep(200)
undo()
undo = expand(1, 7)
await sleep(100)
undo()
undo = expand(1, 4)
await sleep(100)
undo()
undo = expand(1, 2)
await sleep(100)
undo()
circles[0].r = inicr1
circles[1].r = inicr2
}
// 連続描画
const { quit, stop, restart } = setAnimationFrame(drawFrame, 10)
initiate()