プログラム人生の中で一度はハートを描いてくれって言われたことありませんか?
ハートを描いてみる
というわけで早速ハートを描いてみます。
オーソドックスなハートってこんな形な気がします。
これ、こんな風に点をとって描いてます。
原点をハートの先っちょに持ってってやって、ハートの幅を w、高さを h として図をみると、右半分のパスは次の3つで書けそうです。左半分はそれの鏡像でいいでしょう。
三次ベジエ:(0,0)-(x,y)-(w/2,y1)-(w/2,y2)
円弧:(w/2,y2)-(w/2,h)-(x1,h)
円弧:(x1,h)-(x2,h)-(0,dy)
canvas.getContext( '2d' )で取れるCanvasRenderingContext2D
にハートの形のパスを構成するheartPath
を仕込んじゃいましょう。
円弧を描く CanvasRenderingContext2D.arc はパラメータが面倒なので、三次ベジエで近似します。
// Curve factor : 0.552285
const CF = ( -24 + Math.sqrt( 24 * 24 + 64 * 9 ) ) / 18
CanvasRenderingContext2D.prototype.heartPath = function( w, h, x, y, y1, y2, x1, x2, dy ) {
x *= w
y *= h
y1 *= h
y2 *= h
x1 *= w
x2 *= w
dy *= h
let cx = 0
let cy = 0
const
MoveTo = ( x, y ) => {
this.moveTo( x, y )
cx = x
cy = y
}
const
CurveTo = ( x1, y1, x2, y2, x, y ) => {
this.bezierCurveTo( x1, y1, x2, y2, x, y )
cx = x
cy = y
}
const
ArcByQ = ( x1, y1, x, y ) => {
CurveTo(
x1 * CF + cx * ( 1 - CF )
, y1 * CF + cy * ( 1 - CF )
, x1 * CF + x * ( 1 - CF )
, y1 * CF + y * ( 1 - CF )
, x
, y
)
}
const rH = w / 2
MoveTo( 0, 0 )
CurveTo( -x, y, -rH, y1, -rH, y2 )
ArcByQ( -rH, h, -x1, h )
ArcByQ( -x2, h, 0, dy )
ArcByQ( +x2, h, +x1, h )
ArcByQ( rH, h, rH, y2 )
CurveTo( rH, y1, x, y, 0, 0 )
}
このオーソドックスなハートは以下のパラメータで描いています。
const canvas = document.createElement( 'canvas' )
const w = 512
canvas.width = w * 1.2
const h = 512
canvas.height = h * 1.2
const x = 0.05
const y = 0.10
const y1 = 0.40
const y2 = 0.72
const x1 = 0.25
const x2 = 0.05
const dy = 0.84
c2d.translate( w / 2, h )
c2d.scale( 1, -1 )
c2d.translate( canvas.width / 2, h )
c2d.heartPath( w, h, x, y, y1, y2, x1, x2, dy )
c2d.fillStyle = 'red'
c2d.fill()
ハートをエロくする
本題です。
プログラム人生の中で、一度はハートをエロくしてくれって言われたことありませんか?
というわけで、早速ハートをエロくしてみます。
ハートマークの下部をしぼって、上部が円形になるようにすると、エロくなるみたいです。
要するに女体っぽくすればいいようです。
WebComponent 化して、CodePen でみてみます。
See the Pen yLYqvdP by Satachito (@satachito) on CodePen.
CodePenが失敗したときの保険のためにイメージを貼っておきます。
下のようなパラメータにしてみました。エロいかどうかは主観に基づいているのでほんとにエロく感じていただけているか不安なのですがいかがでしょうか?
<heart-element w=200 h=200 x=0.25 y=0.50 y1=0.50 y2=0.75 x1=0.25 x2=0.05 dy=0.80></heart-element>
バレンタインのチョコの箱
ちょっと季節はずれな話題ですが、バレンタインの時にみるハート型のチョコの箱の形、チョコを入れるという役割とのトレードオフなのはわかりますが、なんか違和感ありますよね。
というわけで、バレンタインのチョコの箱を描いてみます。こんなパラメータでどうでしょう。
上2つと違うのは、y
を0
にして、先っちょの部分も接線接続しているところです。
<heart-element w=240 h=200 x=0.10 y=0 y1=0.30 y2=0.70 x1=0.20 x2=0.10 dy=0.93></heart-element>
最後に
エロいハートの作り方を知りたくて「エロいハート」でググったら仕事にならなくなりました。同様の悩みを抱えている方のために最後にこの記事で使った画像を全部表示するスタンドアローンで動く(ファイルをブラウザに食わせて動く)html ファイルを載せておきます。
<script type=module>
// Curve factor : 0.552285
const CF = ( -24 + Math.sqrt( 24 * 24 + 64 * 9 ) ) / 18
CanvasRenderingContext2D.prototype.heartPath = function( w, h, x, y, y1, y2, x1, x2, dy ) {
x *= w
y *= h
y1 *= h
y2 *= h
x1 *= w
x2 *= w
dy *= h
let cx = 0
let cy = 0
const
MoveTo = ( x, y ) => {
this.moveTo( x, y )
cx = x
cy = y
}
const
CurveTo = ( x1, y1, x2, y2, x, y ) => {
this.bezierCurveTo( x1, y1, x2, y2, x, y )
cx = x
cy = y
}
const
ArcByQ = ( x1, y1, x, y ) => {
CurveTo(
x1 * CF + cx * ( 1 - CF )
, y1 * CF + cy * ( 1 - CF )
, x1 * CF + x * ( 1 - CF )
, y1 * CF + y * ( 1 - CF )
, x
, y
)
}
const rH = w / 2
MoveTo( 0, 0 )
CurveTo( -x, y, -rH, y1, -rH, y2 )
ArcByQ( -rH, h, -x1, h )
ArcByQ( -x2, h, 0, dy )
ArcByQ( +x2, h, +x1, h )
ArcByQ( rH, h, rH, y2 )
CurveTo( rH, y1, x, y, 0, 0 )
}
class
HeartElement extends HTMLElement {
constructor() {
super()
const canvas = document.createElement( 'canvas' )
const w = this.getAttribute( 'w' )
const h = this.getAttribute( 'h' )
canvas.setAttribute( 'width', w )
canvas.setAttribute( 'height', h )
this.appendChild( canvas )
const c2d = canvas.getContext( '2d' )
c2d.translate( w / 2, h )
c2d.scale( 1, -1 )
c2d.heartPath(
w
, h
, Number( this.getAttribute( 'x' ) )
, Number( this.getAttribute( 'y' ) )
, Number( this.getAttribute( 'y1' ) )
, Number( this.getAttribute( 'y2' ) )
, Number( this.getAttribute( 'x1' ) )
, Number( this.getAttribute( 'x2' ) )
, Number( this.getAttribute( 'dy' ) )
)
c2d.fillStyle = 'red'
c2d.fill()
}
}
customElements.define( 'heart-element', HeartElement )
class
HeartExplanation extends HTMLElement {
constructor() {
super()
const canvas = document.createElement( 'canvas' )
this.appendChild( canvas )
const c2d = canvas.getContext( '2d' )
const w = 512
canvas.width = w * 1.2
const h = 512
canvas.height = h * 1.2
const x = 0.05
const y = 0.10
const y1 = 0.40
const y2 = 0.72
const x1 = 0.25
const x2 = 0.05
const dy = 0.84
c2d.translate( canvas.width / 2, h * 1.1 )
c2d.font = '24px serif'
c2d.fillText( '原点(0,0)', 0, 24 )
c2d.fillText( 'x, y', 4 + x * w, -y * h )
c2d.fillText( 'y1', 4 + w / 2, -y1 * h )
c2d.fillText( 'y2', 4 + w / 2, -y2 * h )
c2d.fillText( '(w/2,h)', -36 + w / 2, -h - 4 )
c2d.fillText( 'x1', x1 * w, -h - 4 )
c2d.fillText( 'x2', x2 * w, -h - 4 )
c2d.fillText( 'dy', 0, -dy * h + 24 )
c2d.scale( 1, -1 )
c2d.heartPath( w, h, x, y, y1, y2, x1, x2, dy )
c2d.stroke()
c2d.beginPath()
c2d.moveTo( 0, 0 )
c2d.lineTo( x * w, y * h )
c2d.moveTo( w / 2, y1 * h )
c2d.lineTo( w / 2, h )
c2d.lineTo( x2 * w, h )
c2d.lineTo( 0, dy * h )
c2d.strokeStyle = 'red'
c2d.stroke()
const
FillCircle = ( x, y ) => {
c2d.beginPath()
c2d.ellipse( x, y, 4, 4, 0, 0, 2 * Math.PI )
c2d.fill()
}
c2d.fillStyle = 'blue'
FillCircle( x * w, y * h )
FillCircle( w / 2, y1 * h )
FillCircle( w / 2, y2 * h )
FillCircle( w / 2, h )
FillCircle( x1 * w, h )
FillCircle( x2 * w, h )
FillCircle( 0, dy * h )
}
}
customElements.define( 'heart-explanation', HeartExplanation )
</script>
<heart-explanation></heart-explanation>
<div>オーソドックス</div>
<heart-element w=200 h=200 x=0.05 y=0.10 y1=0.40 y2=0.72 x1=0.25 x2=0.05 dy=0.84></heart-element>
<br>
<br>
<div>ちょっとエロい?</div>
<heart-element w=200 h=200 x=0.25 y=0.50 y1=0.50 y2=0.75 x1=0.25 x2=0.05 dy=0.80></heart-element>
<br>
<br>
<div>バレンタインのチョコの箱</div>
<heart-element w=240 h=200 x=0.10 y=0 y1=0.30 y2=0.70 x1=0.20 x2=0.10 dy=0.93></heart-element>
<br>
<br>
<style>
* {
; margin : 0
; padding : 0
}
</style>